From bc1d31df4040579a4cccae9bd51dc67bf08951db Mon Sep 17 00:00:00 2001 From: =?utf8?q?F=C3=A9lix=20Sipma?= Date: Mon, 4 Feb 2019 10:06:20 +0000 Subject: [PATCH] Import patat_0.8.2.1.orig.tar.gz [dgit import orig patat_0.8.2.1.orig.tar.gz] --- .circleci/config.yml | 37 ++ .circleci/release.sh | 46 ++ .circleci/tickle.sh | 24 + .gitignore | 7 + CHANGELOG.md | 157 ++++++ LICENSE | 339 ++++++++++++ Makefile | 26 + README.md | 584 ++++++++++++++++++++ Setup.hs | 2 + extra/make-man.hs | 122 ++++ extra/screenshot.png | Bin 0 -> 52076 bytes patat.cabal | 102 ++++ src/Data/Aeson/Extended.hs | 22 + src/Data/Aeson/TH/Extended.hs | 21 + src/Data/Data/Extended.hs | 23 + src/Main.hs | 191 +++++++ src/Patat/AutoAdvance.hs | 52 ++ src/Patat/Images.hs | 60 ++ src/Patat/Images/ITerm2.hs | 56 ++ src/Patat/Images/Internal.hs | 39 ++ src/Patat/Images/W3m.hs | 145 +++++ src/Patat/Presentation.hs | 20 + src/Patat/Presentation/Display.hs | 377 +++++++++++++ src/Patat/Presentation/Display/CodeBlock.hs | 83 +++ src/Patat/Presentation/Display/Table.hs | 107 ++++ src/Patat/Presentation/Fragment.hs | 134 +++++ src/Patat/Presentation/Interactive.hs | 126 +++++ src/Patat/Presentation/Internal.hs | 266 +++++++++ src/Patat/Presentation/Read.hs | 205 +++++++ src/Patat/PrettyPrint.hs | 411 ++++++++++++++ src/Patat/Theme.hs | 324 +++++++++++ src/Text/Pandoc/Extended.hs | 30 + stack.yaml | 15 + test.sh | 30 + tests/01.md | 14 + tests/01.md.dump | 8 + tests/02.lhs | 6 + tests/02.lhs.dump | 8 + tests/03.md | 46 ++ tests/03.md.dump | 48 ++ tests/bolditalic.md | 8 + tests/bolditalic.md.dump | 1 + tests/comments.md | 16 + tests/comments.md.dump | 8 + tests/deflist.md | 20 + tests/deflist.md.dump | 24 + tests/extentions0.md | 9 + tests/extentions0.md.dump | 1 + tests/extentions1.md | 7 + tests/extentions1.md.dump | 1 + tests/fragments.md | 27 + tests/fragments.md.dump | 54 ++ tests/headers.md | 15 + tests/headers.md.dump | 21 + tests/links.md | 8 + tests/links.md.dump | 10 + tests/lists.md | 13 + tests/lists.md.dump | 15 + tests/margins.md | 17 + tests/margins.md.dump | 10 + tests/meta.md | 12 + tests/meta.md.dump | 7 + tests/slidelevel0.md | 12 + tests/slidelevel0.md.dump | 7 + tests/slidelevel1.md | 26 + tests/slidelevel1.md.dump | 22 + tests/slidelevel2.md | 15 + tests/slidelevel2.md.dump | 21 + tests/syntax.md | 14 + tests/syntax.md.dump | 7 + tests/tables.md | 48 ++ tests/tables.md.dump | 48 ++ tests/themes.md | 12 + tests/themes.md.dump | 5 + tests/wrapping.md | 25 + tests/wrapping.md.dump | 20 + 76 files changed, 4899 insertions(+) create mode 100644 .circleci/config.yml create mode 100755 .circleci/release.sh create mode 100755 .circleci/tickle.sh create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 Setup.hs create mode 100644 extra/make-man.hs create mode 100644 extra/screenshot.png create mode 100644 patat.cabal create mode 100644 src/Data/Aeson/Extended.hs create mode 100644 src/Data/Aeson/TH/Extended.hs create mode 100644 src/Data/Data/Extended.hs create mode 100644 src/Main.hs create mode 100644 src/Patat/AutoAdvance.hs create mode 100644 src/Patat/Images.hs create mode 100644 src/Patat/Images/ITerm2.hs create mode 100644 src/Patat/Images/Internal.hs create mode 100644 src/Patat/Images/W3m.hs create mode 100644 src/Patat/Presentation.hs create mode 100644 src/Patat/Presentation/Display.hs create mode 100644 src/Patat/Presentation/Display/CodeBlock.hs create mode 100644 src/Patat/Presentation/Display/Table.hs create mode 100644 src/Patat/Presentation/Fragment.hs create mode 100644 src/Patat/Presentation/Interactive.hs create mode 100644 src/Patat/Presentation/Internal.hs create mode 100644 src/Patat/Presentation/Read.hs create mode 100644 src/Patat/PrettyPrint.hs create mode 100644 src/Patat/Theme.hs create mode 100644 src/Text/Pandoc/Extended.hs create mode 100644 stack.yaml create mode 100755 test.sh create mode 100644 tests/01.md create mode 100644 tests/01.md.dump create mode 100644 tests/02.lhs create mode 100644 tests/02.lhs.dump create mode 100644 tests/03.md create mode 100644 tests/03.md.dump create mode 100644 tests/bolditalic.md create mode 100644 tests/bolditalic.md.dump create mode 100644 tests/comments.md create mode 100644 tests/comments.md.dump create mode 100644 tests/deflist.md create mode 100644 tests/deflist.md.dump create mode 100644 tests/extentions0.md create mode 100644 tests/extentions0.md.dump create mode 100644 tests/extentions1.md create mode 100644 tests/extentions1.md.dump create mode 100644 tests/fragments.md create mode 100644 tests/fragments.md.dump create mode 100644 tests/headers.md create mode 100644 tests/headers.md.dump create mode 100644 tests/links.md create mode 100644 tests/links.md.dump create mode 100644 tests/lists.md create mode 100644 tests/lists.md.dump create mode 100644 tests/margins.md create mode 100644 tests/margins.md.dump create mode 100644 tests/meta.md create mode 100644 tests/meta.md.dump create mode 100644 tests/slidelevel0.md create mode 100644 tests/slidelevel0.md.dump create mode 100644 tests/slidelevel1.md create mode 100644 tests/slidelevel1.md.dump create mode 100644 tests/slidelevel2.md create mode 100644 tests/slidelevel2.md.dump create mode 100644 tests/syntax.md create mode 100644 tests/syntax.md.dump create mode 100644 tests/tables.md create mode 100644 tests/tables.md.dump create mode 100644 tests/themes.md create mode 100644 tests/themes.md.dump create mode 100644 tests/wrapping.md create mode 100644 tests/wrapping.md.dump diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..4d7780d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,37 @@ +version: 2 + +workflows: + version: 2 + build-workflow: + jobs: + - build: + filters: + tags: + only: /.*/ + +jobs: + build: + # This image has most Haskell stuff preinstalled. + docker: + - image: 'fpco/stack-build:latest' + + steps: + - checkout + - restore_cache: + key: 'v4-patat-{{ arch }}-{{ .Branch }}' + - run: + # We set jobs to 1 here because that prevents Out-Of-Memory exceptions + # while compiling dependencies. + name: 'Install' + command: '.circleci/tickle.sh stack build --pedantic --copy-bins --jobs=1 --no-terminal' + - run: + name: 'Run tests' + command: 'make test' + - save_cache: + key: 'v4-patat-{{ arch }}-{{ .Branch }}-{{ .Revision }}' + paths: + - '~/.stack-work' + - '~/.stack' + - run: + name: 'Upload release' + command: '.circleci/release.sh "$CIRCLE_TAG"' diff --git a/.circleci/release.sh b/.circleci/release.sh new file mode 100755 index 0000000..b5f7f76 --- /dev/null +++ b/.circleci/release.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -o nounset -o errexit -o pipefail + +TAG="$1" +SUFFIX="linux-$(uname -m)" +USER="jaspervdj" +REPOSITORY="$(basename -- *.cabal ".cabal")" +BINARY="$REPOSITORY" + +echo "Tag: $TAG" +echo "Suffix: $SUFFIX" +echo "Repository: $REPOSITORY" + +$BINARY --version + +if [[ -z "$TAG" ]]; then + echo "Not a tagged build, skipping release..." + exit 0 +fi + +# Install ghr +GHR_VERSION="v0.5.4" +wget --quiet \ + "https://github.com/tcnksm/ghr/releases/download/${GHR_VERSION}/ghr_${GHR_VERSION}_linux_386.zip" +unzip ghr_${GHR_VERSION}_linux_386.zip + +# Install upx +UPX_VERSION="3.94" +wget --quiet \ + "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz" +tar xf upx-${UPX_VERSION}-amd64_linux.tar.xz +mv upx-${UPX_VERSION}-amd64_linux/upx . + +# Create tarball +PACKAGE="$REPOSITORY-$TAG-$SUFFIX" +mkdir -p "$PACKAGE" +cp "$(which "$BINARY")" "$PACKAGE" +./upx -q "$PACKAGE/$BINARY" +cp README.* "$PACKAGE" +cp CHANGELOG.* "$PACKAGE" +cp extra/patat.1 "$PACKAGE" +tar -czf "$PACKAGE.tar.gz" "$PACKAGE" +rm -r "$PACKAGE" + +# Actually upload +./ghr -u "$USER" -r "$REPOSITORY" "$TAG" "$PACKAGE.tar.gz" diff --git a/.circleci/tickle.sh b/.circleci/tickle.sh new file mode 100755 index 0000000..195c29c --- /dev/null +++ b/.circleci/tickle.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -o nounset -o errexit -o pipefail + +function tickle() { + while [ true ]; do + echo "[$(date +%H:%M:%S)] Tickling..." + sleep 60 + done +} + +echo "Forking tickle process..." +tickle & +TICKLE_PID=$! + +echo "Forking build process..." +eval $@ & +BUILD_PID=$! + +echo "Waiting for build thread ($BUILD_PID)..." +wait $BUILD_PID + +echo "Killing tickle thread ($TICKLE_PID)..." +kill $TICKLE_PID +echo "All done!" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da4d999 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.o +*.hi +extra/make-man +extra/patat.1 +.stack-work +dist +tags diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b3b72e9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,157 @@ +# Changelog + +- 0.8.2.1 (2019-02-03) + * Bump `pandoc` to 2.6 + * Bump `ansi-terminal` to 0.10 + +- 0.8.2.0 (2019-01-24) + * GHC 7.8 compatibility + +- 0.8.1.3 (2019-01-24) + * Bump `pandoc` to 2.4 + * Bump `yaml` to 0.11 + +- 0.8.1.2 (2018-10-29) + * Work around test failure caused by slightly different syntax highlighting + in different pandoc versions + +- 0.8.1.1 (2018-10-26) + * Tickle CircleCI cache + +- 0.8.1.0 (2018-10-26) + * Add support for italic ansi code in themes + * Fix centered titles not being centered (contribution by Hamza Haiken) + +- 0.8.0.0 (2018-08-31) + * Themed border rendering improvements (contribution by Hamza Haiken) + * Add support for margins (contribution by Hamza Haiken) + * Add RGB colour support for themes (contribution by Hamza Haiken) + * Add experimental images support + * Add images support for iTerm2 (contribution by @2mol) + +- 0.7.2.0 (2018-05-08) + * GHC 8.4 compatibility + +- 0.7.1.0 (2018-05-08) + * GHC 8.4 compatibility + +- 0.7.0.0 (2018-05-04) + * Support HTML-style comments + +- 0.6.1.2 (2018-04-30) + * Bump `pandoc` to 2.2 + +- 0.6.1.1 (2018-04-27) + * Bump `aeson` to 1.3 + * Bump `skylighting` to 0.7 + * Bump `time` to 1.9 + * Bump `ansi-terminal` to 0.8 + +- 0.6.1.0 (2018-01-28) + * Bump `skylighting` to 0.6 + * Bump `pandoc` to 2.1 + * Bump `ansi-terminal` to 0.7 + +- 0.6.0.1 (2017-12-24) + * Automatically upload linux binary to GitHub + +- 0.6.0.0 (2017-12-19) + * Make pandoc extensions customizable in the configuration + * Bump `pandoc` to 2.0 + +- 0.5.2.2 (2017-06-14) + * Add `network-uri` dependency to fix travis build + +- 0.5.2.1 (2017-06-14) + * Bump `optparse-applicative-0.14` dependency + +- 0.5.2.0 (2017-05-16) + * Add navigation using `PageUp` and `PageDown`. + * Use `skylighting` instead of deprecated `highlighting-kate` for syntax + highlighting. + +- 0.5.1.2 (2017-04-26) + * Make build reproducible even if timezone changes (patch by Félix Sipma) + +- 0.5.1.1 (2017-04-23) + * Include `README` in `Extra-source-files` so it gets displayed on Hackage + +- 0.5.1.0 (2017-04-23) + * Bump `aeson-1.2` dependency + * Fix vertical alignment of title slides + * Fix wrapping issue with inline code at end of line + * Add bash-completion script generation to Makefile + +- 0.5.0.0 (2017-02-06) + * Add a `slideLevel` option & autodetect it. This changes the way `patat` + splits slides. For more information, see the `README` or the `man` page. + If you just want to get the old behavior back, just add: + + --- + patat: + slideLevel: 1 + ... + + To the top of your presentation. + + * Clear the screen when finished with the presentation. + +- 0.4.7.1 (2017-01-22) + * Bump `directory-1.3` dependency + * Bump `time-1.7` dependency + +- 0.4.7.0 (2017-01-20) + * Bump `aeson-1.1` dependency + * Parse YAML for settings using `yaml` instead of pandoc + * Clarify watch & autoAdvance combination in documentation. + +- 0.4.6.0 (2016-12-28) + * Redraw the screen on unknown commands to prevent accidental typing from + showing up. + * Make the cursor invisible during the presentation. + * Move the footer down one more line to gain some screen real estate. + +- 0.4.5.0 (2016-12-05) + * Render the date in a locale-independent manner (patch by Daniel + Shahaf). + +- 0.4.4.0 (2016-12-03) + * Force the use of UTF-8 when generating the man page. + +- 0.4.3.0 (2016-12-02) + * Use `SOURCE_DATE_EPOCH` if it is present instead of getting the date from + `git log`. + +- 0.4.2.0 (2016-12-01) + * Fix issues with man page generation on Travis. + +- 0.4.1.0 (2016-12-01) + * Fix compatibility with `pandoc-1.18` and `pandoc-1.19`. + * Add a man page. + +- 0.4.0.0 (2016-11-15) + * Add configurable auto advancing. + * Support fragmented slides. + +- 0.3.3.0 (2016-10-31) + * Add a `--version` flag. + * Add support for `pandoc-1.18` which includes a new `LineBlock` element. + +- 0.3.2.0 (2016-10-20) + * Keep running even if errors are encountered during reload. + +- 0.3.1.0 (2016-10-18) + * Fix compilation with `lts-6.22`. + +- 0.3.0.0 (2016-10-17) + * Add syntax highlighting support. + * Fixed slide clipping after reload. + +- 0.2.0.0 (2016-10-13) + * Add theming support. + * Fix links display. + * Add support for wrapping. + * Allow org mode as input format. + +- 0.1.0.0 (2016-10-02) + * Upload first version from hotel wifi in Kalaw. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f53f40 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d8513a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +# We use `?=` to set SOURCE_DATE_EPOCH only if it is not present. Unfortunately +# we can't use `git --date=unix` since only very recent git versions support +# that, so we need to make a round trip through `date`. +SOURCE_DATE_EPOCH?=$(shell date '+%s' \ + --date="$(shell git log -1 --format=%cd --date=rfc)") + +extra/patat.1: README.md + SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" patat-make-man >$@ + +extra/patat.bash-completion: + patat --bash-completion-script patat >$@ + +completion: extra/patat.bash-completion + +man: extra/patat.1 + +# Also check if we can generate the manual. +test: man + bash test.sh + +clean: + rm -f extra/patat.1 + rm -f extra/make-man + rm -f extra/patat.bash-completion + +.PHONY: man completion test clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0ae34c --- /dev/null +++ b/README.md @@ -0,0 +1,584 @@ +patat +===== + +[![Build Status](https://img.shields.io/circleci/project/github/jaspervdj/patat.svg)](https://circleci.com/gh/jaspervdj/patat) [![Hackage](https://img.shields.io/hackage/v/patat.svg)](https://hackage.haskell.org/package/patat) [![GitHub tag](https://img.shields.io/github/tag/jaspervdj/patat.svg)]() + +`patat` (**P**resentations **A**top **T**he **A**NSI **T**erminal) is a small +tool that allows you to show presentations using only an ANSI terminal. It does +not require `ncurses`. + +Features: + +- Leverages the great [Pandoc] library to support many input formats including + [Literate Haskell]. +- Supports [smart slide splitting](#input-format). +- Slides can be split up into [multiple fragments](#fragmented-slides) +- There is a [live reload](#running) mode. +- [Theming](#theming) support including 24-bit RGB. +- [Auto advancing](#auto-advancing) with configurable delay. +- Optionally [re-wrapping](#line-wrapping) text to terminal width with proper + indentation. +- Syntax highlighting for nearly one hundred languages generated from [Kate] + syntax files. +- Experimental [images](#images) support. +- Written in [Haskell]. + +![screenshot](extra/screenshot.png?raw=true) + +[Kate]: https://kate-editor.org/ +[Haskell]: http://haskell.org/ +[Pandoc]: http://pandoc.org/ + +Table of Contents +----------------- + +- [Table of Contents](#table-of-contents) +- [Installation](#installation) + - [Pre-built-packages](#pre-built-packages) + - [From source](#from-source) +- [Running](#running) +- [Options](#options) +- [Controls](#controls) +- [Input format](#input-format) +- [Configuration](#configuration) + - [Line wrapping](#line-wrapping) + - [Auto advancing](#auto-advancing) + - [Advanced slide splitting](#advanced-slide-splitting) + - [Fragmented slides](#fragmented-slides) + - [Theming](#theming) + - [Syntax Highlighting](#syntax-highlighting) + - [Pandoc Extensions](#pandoc-extensions) + - [Images](#images) +- [Trivia](#trivia) + +Installation +------------ + +### Pre-built-packages + +- Archlinux: +- Debian: +- Ubuntu: +- openSUSE: + +You can also find generic linux binaries here: +. + +### From source + +Installation from source is very easy. You can build from source using `stack +install` or `cabal install`. `patat` is also available from [Hackage]. + +[Hackage]: https://hackage.haskell.org/package/patat + +For people unfamiliar with the Haskell ecosystem, this means you can do either +of the following: + +#### Using stack + +1. Install [stack] for your platform. +2. Clone this repository. +3. Run `stack setup` (if you're running stack for the first time) and + `stack install`. +4. Make sure `$HOME/.local/bin` is in your `$PATH`. + +[stack]: https://docs.haskellstack.org/en/stable/README/ + +#### Using cabal + +1. Install [cabal] for your platform. +2. Run `cabal install patat`. +3. Make sure `$HOME/.cabal/bin` is in your `$PATH`. + +[cabal]: https://www.haskell.org/cabal/ + +Running +------- + +`patat [*options*] file` + +Options +------- + +`-w`, `--watch` + +: If you provide the `--watch` flag, `patat` will watch the presentation file + for changes and reload automatically. This is very useful when you are + writing the presentation. + +`-f`, `--force` + +: Run the presentation even if the terminal claims it does not support ANSI + features. + +`-d`, `--dump` + +: Just dump all the slides to stdout. This is useful for debugging. + +`--version` + +: Display version information. + +Controls +-------- + +- **Next slide**: `space`, `enter`, `l`, `→`, `PageDown` +- **Previous slide**: `backspace`, `h`, `←`, `PageUp` +- **Go forward 10 slides**: `j`, `↓` +- **Go backward 10 slides**: `k`, `↑` +- **First slide**: `0` +- **Last slide**: `G` +- **Reload file**: `r` +- **Quit**: `q` + +The `r` key is very useful since it allows you to preview your slides while you +are writing them. You can also use this to fix artifacts when the terminal is +resized. + +Input format +------------ + +The input format can be anything that Pandoc supports. Plain markdown is +usually the most simple solution: + +```markdown +--- +title: This is my presentation +author: Jane Doe +... + +# This is a slide + +Slide contents. Yay. + +--- + +# Important title + +Things I like: + +- Markdown +- Haskell +- Pandoc +``` + +Horizontal rulers (`---`) are used to split slides. + +However, if you prefer not use these since they are a bit intrusive in the +markdown, you can also start every slide with a header. In that case, the file +should not contain a single horizontal ruler. + +`patat` will pick the most deeply nested header (e.g. `h2`) as the marker for a +new slide. Headers _above_ the most deeply nested header (e.g. `h1`) will turn +into title slides, which are displayed as as a slide containing only the +centered title. + +This means the following document is equivalent to the one we saw before: + +```markdown +--- +title: This is my presentation +author: Jane Doe +... + +# This is a slide + +Slide contents. Yay. + +# Important title + +Things I like: + +- Markdown +- Haskell +- Pandoc +``` + +And that following document contains three slides: a title slide, followed by +two content slides. + +```markdown +--- +title: This is my presentation +author: Jane Doe +... + +# Chapter 1 + +## This is a slide + +Slide contents. Yay. + +## Another slide + +Things I like: + +- Markdown +- Haskell +- Pandoc +``` + +For more information, see [Advanced slide splitting](#advanced-slide-splitting). + +Patat supports comments which can be used as speaker notes. + +```markdown +--- +title: This is my presentation +author: Jane Doe +... + +# Chapter 1 + + + +Slide contents. Yay. + + +``` + +Configuration +------------- + +`patat` is fairly configurable. The configuration is done using [YAML]. There +are two places where you can put your configuration: + +1. In the presentation file itself, using the [Pandoc metadata header]. +2. In `$HOME/.patat.yaml` + +[YAML]: http://yaml.org/ +[Pandoc metadata header]: http://pandoc.org/MANUAL.html#extension-yaml_metadata_block + +For example, we set an option `key` to `val` by using the following file: + +```markdown +--- +title: Presentation with options +author: John Doe +patat: + key: val +... + +Hello world. +``` + +Or we can use a normal presentation and have the following `$HOME/.patat.yaml`: + + key: val + +### Line wrapping + +Line wrapping can be enabled by setting `wrap: true` in the configuration. This +will re-wrap all lines to fit the terminal width better. + +### Margins + +Margins can be enabled by setting a `margins` entry in the configuration: + +```markdown +--- +title: Presentation with margins +author: John Doe +patat: + wrap: true + margins: + left: 10 + right: 10 +... + +Lorem ipsum dolor sit amet, ... +``` + +This example configuration will generate slides with a margin of 10 characters on the left, +and break lines 10 characters before they reach the end of the terminal's width. + +It is recommended to enable [line wrapping](#line-wrapping) along with this feature. + +### Auto advancing + +By setting `autoAdvanceDelay` to a number of seconds, `patat` will automatically +advance to the next slide. + +```markdown +--- +title: Auto-advance, yes please +author: John Doe +patat: + autoAdvanceDelay: 2 +... + +Hello World! + +--- + +This slide will be shown two seconds after the presentation starts. +``` + +Note that changes to `autoAdvanceDelay` are not picked up automatically if you +are running `patat --watch`. This requires restarting `patat`. + +### Advanced slide splitting + +You can control the way slide splitting works by setting the `slideLevel` +variable. This variable defaults to the least header that occurs before a +non-header, but it can also be explicitly defined. For example, in the +following document, the `slideLevel` defaults to **2**: + +```markdown +# This is a slide + +## This is a nested header + +This is some content +``` + +With `slideLevel` 2, the `h1` will turn into a "title slide", and the `h2` will +be displayed at the top of the second slide. We can customize this by setting +`slideLevel` manually: + +```markdown +--- +patat: + slideLevel: 1 +... + +# This is a slide + +## This is a nested header + +This is some content +``` + +Now, we will only see one slide, which contains a nested header. + +### Fragmented slides + +By default, slides are always displayed "all at once". If you want to display +them fragment by fragment, there are two ways to do that. The most common +case is that lists should be displayed incrementally. + +This can be configured by settings `incrementalLists` to `true` in the metadata +block: + +```markdown +--- +title: Presentation with incremental lists +author: John Doe +patat: + incrementalLists: true +... + +- This list +- is displayed +- item by item +``` + +Setting `incrementalLists` works on _all_ lists in the presentation. To flip +the setting for a specific list, wrap it in a block quote. This will make the +list incremental if `incrementalLists` is not set, and it will display the list +all at once if `incrementalLists` is set to `true`. + +This example contains a sublist which is also displayed incrementally, and then +a sublist which is displayed all at once (by merit of the block quote). + +```markdown +--- +title: Presentation with incremental lists +author: John Doe +patat: + incrementalLists: true +... + +- This list +- is displayed + + * item + * by item + +- Or sometimes + + > * all at + > * once +``` + +Another way to break up slides is to use a pagraph only containing three dots +separated by spaces. For example, this slide has two pauses: + +```markdown +Legen + +. . . + +wait for it + +. . . + +Dary! +``` + +### Theming + +Colors and other properties can also be changed using this configuration. For +example, we can have: + +```markdown +--- +author: 'Jasper Van der Jeugt' +title: 'This is a test' +patat: + wrap: true + theme: + emph: [vividBlue, onVividBlack, italic] + strong: [bold] + imageTarget: [onDullWhite, vividRed] +... + +# This is a presentation + +This is _emph_ text. + +![Hello](foo.png) +``` + +The properties that can be given a list of styles are: + +`blockQuote`, `borders`, `bulletList`, `codeBlock`, `code`, `definitionList`, +`definitionTerm`, `emph`, `header`, `imageTarget`, `imageText`, `linkTarget`, +`linkText`, `math`, `orderedList`, `quoted`, `strikeout`, `strong`, +`tableHeader`, `tableSeparator` + +The accepted styles are: + +`bold`, `italic`, `dullBlack`, `dullBlue`, `dullCyan`, `dullGreen`, +`dullMagenta`, `dullRed`, `dullWhite`, `dullYellow`, `onDullBlack`, +`onDullBlue`, `onDullCyan`, `onDullGreen`, `onDullMagenta`, `onDullRed`, +`onDullWhite`, `onDullYellow`, `onVividBlack`, `onVividBlue`, `onVividCyan`, +`onVividGreen`, `onVividMagenta`, `onVividRed`, `onVividWhite`, `onVividYellow`, +`underline`, `vividBlack`, `vividBlue`, `vividCyan`, `vividGreen`, +`vividMagenta`, `vividRed`, `vividWhite`, `vividYellow` + +Also accepted are styles of the form `rgb#RrGgBb` and `onRgb#RrGgBb`, where `Rr` +`Gg` and `Bb` are hexadecimal bytes (e.g. `rgb#f08000` for an orange foreground, +and `onRgb#101060` for a deep purple background). Naturally, your terminal +needs to support 24-bit RGB for this to work. When creating portable +presentations, it might be better to stick with the named colours listed above. + +### Syntax Highlighting + +As part of theming, syntax highlighting is also configurable. This can be +configured like this: + +```markdown +--- +patat: + theme: + syntaxHighlighting: + decVal: [bold, onDullRed] +... + +... +``` + +`decVal` refers to "decimal values". This is known as a "token type". For a +full list of token types, see [this list] -- the names are derived from there in +an obvious way. + +[this list]: https://hackage.haskell.org/package/highlighting-kate-0.6.3/docs/Text-Highlighting-Kate-Types.html#t:TokenType + +### Pandoc Extensions + +Pandoc comes with a fair number of extensions on top of markdown, listed [here](https://hackage.haskell.org/package/pandoc-2.0.5/docs/Text-Pandoc-Extensions.html). + +`patat` enables a number of them by default, but this is also customizable. + +In order to enable an additional extensions, e.g. `autolink_bare_uris`, add it +to the `pandocExtensions` field in the YAML metadata: + +```markdown +--- +patat: + pandocExtensions: + - patat_extensions + - autolink_bare_uris +... + +Document content... +``` + +The `patat_extensions` in the above snippet refers to the default set of +extensions enabled by `patat`. If you want to disable those and only use a +select few extensions, simply leave it out and choose your own: + +```markdown +--- +patat: + pandocExtensions: + - autolink_bare_uris + - emoji +... + +... + +Document content... +``` + +If you don't want to enable any extensions, simply set `pandocExtensions` to the +empty list `[]`. + + +### Images + +`patat-0.8.0.0` and newer include images support for some terminal emulators. + +```markdown +--- +patat: + images: + backend: auto +... + +# A slide with only an image. + +![](matterhorn.jpg) +``` + +If `images` is enabled (not by default), `patat` will draw slides that consist +only of a single image just by drawing the image, centered and resized to fit +the terminal window. + +`patat` supports the following image drawing backends: + +- `backend: iterm2`: uses [iTerm2](https://iterm2.com/)'s special escape + sequence to render the image. This even works with animated GIFs! + +- `backend: w3m`: uses the `w3mimgdisplay` executable to draw directly onto + the window. This has been tested in `urxvt` and `xterm`, but is known to + produce weird results in `tmux`. + + If `w3mimgdisplay` is in a non-standard location, you can specify that using + `path`: + + ```yaml + backend: 'w3m' + path: '/home/jasper/.local/bin/w3mimgdisplay' + ``` + +Trivia +------ + +_"Patat"_ is the Flemish word for a simple potato. Dutch people also use it to +refer to French Fries but I don't really do that -- in Belgium we just call +fries _"Frieten"_. + +The idea of `patat` is largely based upon [MDP] which is in turn based upon +[VTMC]. I wanted to write a clone using Pandoc because I ran into a markdown +parsing bug in MDP which I could not work around. A second reason to do a +Pandoc-based tool was that I would be able to use [Literate Haskell] as well. +Lastly, I also prefer not to install Node.js on my machine if I can avoid it. + +[MDP]: https://github.com/visit1985/mdp +[VTMC]: https://github.com/jclulow/vtmc +[Literate Haskell]: https://wiki.haskell.org/Literate_programming diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/extra/make-man.hs b/extra/make-man.hs new file mode 100644 index 0000000..cd14cf0 --- /dev/null +++ b/extra/make-man.hs @@ -0,0 +1,122 @@ +-- | This script generates a man page for patat. +{-# LANGUAGE OverloadedStrings #-} +import Control.Applicative ((<$>)) +import Control.Exception (throw) +import Control.Monad (guard) +import Control.Monad.Trans (liftIO) +import Data.Char (isSpace, toLower) +import Data.List (isPrefixOf) +import Data.Maybe (isJust) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import qualified GHC.IO.Encoding as Encoding +import Prelude +import System.Environment (getEnv) +import qualified System.IO as IO +import qualified Data.Time as Time +import qualified Text.Pandoc as Pandoc + +getVersion :: IO String +getVersion = + dropWhile isSpace . drop 1 . dropWhile (/= ':') . head . + filter (\l -> "version:" `isPrefixOf` map toLower l) . + map (dropWhile isSpace) . lines <$> readFile "patat.cabal" + +getPrettySourceDate :: IO String +getPrettySourceDate = do + epoch <- getEnv "SOURCE_DATE_EPOCH" + utc <- Time.parseTimeM True locale "%s" epoch :: IO Time.UTCTime + return $ Time.formatTime locale "%B %d, %Y" utc + where + locale = Time.defaultTimeLocale + +type Sections = [(Int, T.Text, [Pandoc.Block])] + +toSections :: Int -> [Pandoc.Block] -> Sections +toSections level = go + where + go [] = [] + go (h : xs) = case toSectionHeader h of + Nothing -> go xs + Just (l, title) -> + let (section, cont) = break (isJust . toSectionHeader) xs in + (l, title, section) : go cont + + toSectionHeader :: Pandoc.Block -> Maybe (Int, T.Text) + toSectionHeader (Pandoc.Header l _ inlines) = do + guard (l <= level) + let doc = Pandoc.Pandoc Pandoc.nullMeta [Pandoc.Plain inlines] + txt = case Pandoc.runPure (Pandoc.writeMarkdown Pandoc.def doc) of + Left err -> throw err -- Bad! + Right x -> x + return (l, txt) + toSectionHeader _ = Nothing + +fromSections :: Sections -> [Pandoc.Block] +fromSections = concatMap $ \(level, title, blocks) -> + Pandoc.Header level ("", [], []) [Pandoc.Str $ T.unpack title] : blocks + +reorganizeSections :: Pandoc.Pandoc -> Pandoc.Pandoc +reorganizeSections (Pandoc.Pandoc meta0 blocks0) = + let sections0 = toSections 2 blocks0 in + Pandoc.Pandoc meta0 $ fromSections $ + [ (1, "NAME", nameSection) + ] ++ + [ (1, "SYNOPSIS", s) + | (_, _, s) <- lookupSection "Running" sections0 + ] ++ + [ (1, "DESCRIPTION", []) + ] ++ + [ (2, n, s) + | (_, n, s) <- lookupSection "Controls" sections0 + ] ++ + [ (2, n, s) + | (_, n, s) <- lookupSection "Input format" sections0 + ] ++ + [ (2, n, s) + | (_, n, s) <- lookupSection "Configuration" sections0 + ] ++ + [ (1, "OPTIONS", s) + | (_, _, s) <- lookupSection "Options" sections0 + ] ++ + [ (1, "SEE ALSO", seeAlsoSection) + ] + where + nameSection = mkPara "patat - Presentations Atop The ANSI Terminal" + seeAlsoSection = mkPara "pandoc(1)" + mkPara str = [Pandoc.Para [Pandoc.Str str]] + + lookupSection name sections = + [section | section@(_, n, _) <- sections, name == n] + +main :: IO () +main = Pandoc.runIOorExplode $ do + liftIO $ Encoding.setLocaleEncoding Encoding.utf8 + + let readerOptions = Pandoc.def + { Pandoc.readerExtensions = Pandoc.pandocExtensions + } + + source <- liftIO $ T.readFile "README.md" + pandoc0 <- Pandoc.readMarkdown readerOptions source + template <- Pandoc.getDefaultTemplate "man" + + version <- liftIO getVersion + date <- liftIO getPrettySourceDate + + let writerOptions = Pandoc.def + { Pandoc.writerTemplate = Just template + , Pandoc.writerVariables = + [ ("author", "Jasper Van der Jeugt") + , ("title", "patat manual") + , ("date", date) + , ("footer", "patat v" ++ version) + , ("section", "1") + ] + } + + let pandoc1 = reorganizeSections $ pandoc0 + txt <- Pandoc.writeMan writerOptions pandoc1 + liftIO $ do + T.putStr txt + IO.hPutStrLn IO.stderr "Wrote man page." diff --git a/extra/screenshot.png b/extra/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..e20d771262ad03e0c19be3ba5cd749955c049e5e GIT binary patch literal 52076 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 +Maintainer: Jasper Van der Jeugt +Homepage: http://github.com/jaspervdj/patat +Copyright: 2016 Jasper Van der Jeugt +Category: Text +Build-type: Simple +Cabal-version: >=1.10 + +Extra-source-files: + CHANGELOG.md + README.md + +Source-repository head + Type: git + Location: git://github.com/jaspervdj/patat.git + +Flag patat-make-man + Description: Build the executable to generate the man page + Default: False + Manual: True + +Executable patat + Main-is: Main.hs + Ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N" + Hs-source-dirs: src + Default-language: Haskell2010 + + Build-depends: + aeson >= 0.9 && < 1.5, + ansi-terminal >= 0.6 && < 0.10, + ansi-wl-pprint >= 0.6 && < 0.7, + base >= 4.6 && < 5, + base64-bytestring >= 1.0 && < 1.1, + bytestring >= 0.10 && < 0.11, + colour >= 2.3 && < 2.4, + containers >= 0.5 && < 0.7, + directory >= 1.2 && < 1.4, + filepath >= 1.4 && < 1.5, + mtl >= 2.2 && < 2.3, + optparse-applicative >= 0.12 && < 0.15, + pandoc >= 2.0.4 && < 2.7, + process >= 1.6 && < 1.7, + skylighting >= 0.1 && < 0.8, + terminal-size >= 0.3 && < 0.4, + text >= 1.2 && < 1.3, + time >= 1.4 && < 1.10, + unordered-containers >= 0.2 && < 0.3, + yaml >= 0.8 && < 0.12, + -- We don't even depend on these packages but they can break cabal install + -- because of the conflicting 'Network.URI' module. + network-uri >= 2.6, + network >= 2.6 + + If impl(ghc < 8.0) + Build-depends: + semigroups >= 0.16 && < 0.19 + + Other-modules: + Data.Aeson.Extended + Data.Aeson.TH.Extended + Data.Data.Extended + Patat.AutoAdvance + Patat.Images + Patat.Images.Internal + Patat.Images.W3m + Patat.Images.ITerm2 + Patat.Presentation + Patat.Presentation.Display + Patat.Presentation.Display.CodeBlock + Patat.Presentation.Display.Table + Patat.Presentation.Fragment + Patat.Presentation.Interactive + Patat.Presentation.Internal + Patat.Presentation.Read + Patat.PrettyPrint + Patat.Theme + Paths_patat + Text.Pandoc.Extended + +Executable patat-make-man + Main-is: make-man.hs + Ghc-options: -Wall + Hs-source-dirs: extra + Default-language: Haskell2010 + + If flag(patat-make-man) + Buildable: True + Else + Buildable: False + + Build-depends: + base >= 4.6 && < 5, + mtl >= 2.2 && < 2.3, + pandoc >= 2.0 && < 2.7, + text >= 1.2 && < 1.3, + time >= 1.6 && < 1.10 diff --git a/src/Data/Aeson/Extended.hs b/src/Data/Aeson/Extended.hs new file mode 100644 index 0000000..9b95cec --- /dev/null +++ b/src/Data/Aeson/Extended.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +module Data.Aeson.Extended + ( module Data.Aeson + + , FlexibleNum (..) + ) where + +import Control.Applicative ((<$>)) +import Data.Aeson +import qualified Data.Text as T +import Text.Read (readMaybe) +import Prelude + +-- | This can be parsed from a JSON string in addition to a JSON number. +newtype FlexibleNum a = FlexibleNum {unFlexibleNum :: a} + deriving (Show, ToJSON) + +instance (FromJSON a, Read a) => FromJSON (FlexibleNum a) where + parseJSON (String str) = case readMaybe (T.unpack str) of + Nothing -> fail $ "Could not parse " ++ T.unpack str ++ " as a number" + Just x -> return (FlexibleNum x) + parseJSON val = FlexibleNum <$> parseJSON val diff --git a/src/Data/Aeson/TH/Extended.hs b/src/Data/Aeson/TH/Extended.hs new file mode 100644 index 0000000..0fa5487 --- /dev/null +++ b/src/Data/Aeson/TH/Extended.hs @@ -0,0 +1,21 @@ +-------------------------------------------------------------------------------- +module Data.Aeson.TH.Extended + ( module Data.Aeson.TH + , dropPrefixOptions + ) where + + +-------------------------------------------------------------------------------- +import Data.Aeson.TH +import Data.Char (isUpper, toLower) + + +-------------------------------------------------------------------------------- +dropPrefixOptions :: Options +dropPrefixOptions = defaultOptions + { fieldLabelModifier = dropPrefix + } + where + dropPrefix str = case break isUpper str of + (_, (y : ys)) -> toLower y : ys + _ -> str diff --git a/src/Data/Data/Extended.hs b/src/Data/Data/Extended.hs new file mode 100644 index 0000000..636591e --- /dev/null +++ b/src/Data/Data/Extended.hs @@ -0,0 +1,23 @@ +module Data.Data.Extended + ( module Data.Data + + , grecQ + , grecT + ) where + +import Data.Data + +-- | Recursively find all values of a certain type. +grecQ :: (Data a, Data b) => a -> [b] +grecQ = concat . gmapQ (\x -> maybe id (:) (cast x) $ grecQ x) + +-- | Recursively apply an update to a certain type. +grecT :: (Data a, Data b) => (a -> a) -> b -> b +grecT f x = gmapT (grecT f) (castMap f x) + +castMap :: (Typeable a, Typeable b) => (a -> a) -> b -> b +castMap f x = case cast x of + Nothing -> x + Just y -> case cast (f y) of + Nothing -> x + Just z -> z diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000..f45ae35 --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,191 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Main where + + +-------------------------------------------------------------------------------- +import Control.Applicative ((<$>), (<*>)) +import Control.Concurrent (forkIO, threadDelay) +import qualified Control.Concurrent.Chan as Chan +import Control.Exception (finally) +import Control.Monad (forever, unless, when) +import qualified Data.Aeson.Extended as A +import Data.Monoid (mempty, (<>)) +import Data.Time (UTCTime) +import Data.Version (showVersion) +import qualified Options.Applicative as OA +import Patat.AutoAdvance +import qualified Patat.Images as Images +import Patat.Presentation +import qualified Paths_patat +import Prelude +import qualified System.Console.ANSI as Ansi +import System.Directory (doesFileExist, + getModificationTime) +import System.Exit (exitFailure, exitSuccess) +import qualified System.IO as IO +import qualified Text.PrettyPrint.ANSI.Leijen as PP + + +-------------------------------------------------------------------------------- +data Options = Options + { oFilePath :: !(Maybe FilePath) + , oForce :: !Bool + , oDump :: !Bool + , oWatch :: !Bool + , oVersion :: !Bool + } deriving (Show) + + +-------------------------------------------------------------------------------- +parseOptions :: OA.Parser Options +parseOptions = Options + <$> (OA.optional $ OA.strArgument $ + OA.metavar "FILENAME" <> + OA.help "Input file") + <*> (OA.switch $ + OA.long "force" <> + OA.short 'f' <> + OA.help "Force ANSI terminal" <> + OA.hidden) + <*> (OA.switch $ + OA.long "dump" <> + OA.short 'd' <> + OA.help "Just dump all slides and exit" <> + OA.hidden) + <*> (OA.switch $ + OA.long "watch" <> + OA.short 'w' <> + OA.help "Watch file for changes") + <*> (OA.switch $ + OA.long "version" <> + OA.help "Display version info and exit" <> + OA.hidden) + + +-------------------------------------------------------------------------------- +parserInfo :: OA.ParserInfo Options +parserInfo = OA.info (OA.helper <*> parseOptions) $ + OA.fullDesc <> + OA.header ("patat v" <> showVersion Paths_patat.version) <> + OA.progDescDoc (Just desc) + where + desc = PP.vcat + [ "Terminal-based presentations using Pandoc" + , "" + , "Controls:" + , "- Next slide: space, enter, l, right, pagedown" + , "- Previous slide: backspace, h, left, pageup" + , "- Go forward 10 slides: j, down" + , "- Go backward 10 slides: k, up" + , "- First slide: 0" + , "- Last slide: G" + , "- Reload file: r" + , "- Quit: q" + ] + + +-------------------------------------------------------------------------------- +parserPrefs :: OA.ParserPrefs +parserPrefs = OA.prefs OA.showHelpOnError + + +-------------------------------------------------------------------------------- +errorAndExit :: [String] -> IO a +errorAndExit msg = do + mapM_ (IO.hPutStrLn IO.stderr) msg + exitFailure + + +-------------------------------------------------------------------------------- +assertAnsiFeatures :: IO () +assertAnsiFeatures = do + supports <- Ansi.hSupportsANSI IO.stdout + unless supports $ errorAndExit + [ "It looks like your terminal does not support ANSI codes." + , "If you still want to run the presentation, use `--force`." + ] + + +-------------------------------------------------------------------------------- +main :: IO () +main = do + options <- OA.customExecParser parserPrefs parserInfo + + when (oVersion options) $ do + putStrLn (showVersion Paths_patat.version) + exitSuccess + + filePath <- case oFilePath options of + Just fp -> return fp + Nothing -> OA.handleParseResult $ OA.Failure $ + OA.parserFailure parserPrefs parserInfo OA.ShowHelpText mempty + + errOrPres <- readPresentation filePath + pres <- either (errorAndExit . return) return errOrPres + + unless (oForce options) assertAnsiFeatures + + -- (Maybe) initialize images backend. + images <- traverse Images.new (psImages $ pSettings pres) + + if oDump options + then dumpPresentation pres + else interactiveLoop options images pres + where + interactiveLoop :: Options -> Maybe Images.Handle -> Presentation -> IO () + interactiveLoop options images pres0 = (`finally` cleanup) $ do + IO.hSetBuffering IO.stdin IO.NoBuffering + Ansi.hideCursor + + -- Spawn the initial channel that gives us commands based on user input. + commandChan0 <- Chan.newChan + _ <- forkIO $ forever $ + readPresentationCommand >>= Chan.writeChan commandChan0 + + -- If an auto delay is set, use 'autoAdvance' to create a new one. + commandChan <- case psAutoAdvanceDelay (pSettings pres0) of + Nothing -> return commandChan0 + Just (A.FlexibleNum delay) -> autoAdvance delay commandChan0 + + -- Spawn a thread that adds 'Reload' commands based on the file time. + mtime0 <- getModificationTime (pFilePath pres0) + when (oWatch options) $ do + _ <- forkIO $ watcher commandChan (pFilePath pres0) mtime0 + return () + + let loop :: Presentation -> Maybe String -> IO () + loop pres mbError = do + case mbError of + Nothing -> displayPresentation images pres + Just err -> displayPresentationError pres err + + c <- Chan.readChan commandChan + update <- updatePresentation c pres + case update of + ExitedPresentation -> return () + UpdatedPresentation pres' -> loop pres' Nothing + ErroredPresentation err -> loop pres (Just err) + + loop pres0 Nothing + + cleanup :: IO () + cleanup = do + Ansi.showCursor + Ansi.clearScreen + Ansi.setCursorPosition 0 0 + + +-------------------------------------------------------------------------------- +watcher :: Chan.Chan PresentationCommand -> FilePath -> UTCTime -> IO a +watcher chan filePath mtime0 = do + -- The extra exists check helps because some editors temporarily make the + -- file disappear while writing. + exists <- doesFileExist filePath + mtime1 <- if exists then getModificationTime filePath else return mtime0 + + when (mtime1 > mtime0) $ Chan.writeChan chan Reload + threadDelay (200 * 1000) + watcher chan filePath mtime1 diff --git a/src/Patat/AutoAdvance.hs b/src/Patat/AutoAdvance.hs new file mode 100644 index 0000000..236e0cb --- /dev/null +++ b/src/Patat/AutoAdvance.hs @@ -0,0 +1,52 @@ +-------------------------------------------------------------------------------- +module Patat.AutoAdvance + ( autoAdvance + ) where + + +-------------------------------------------------------------------------------- +import Control.Concurrent (forkIO, threadDelay) +import qualified Control.Concurrent.Chan as Chan +import Control.Monad (forever) +import qualified Data.IORef as IORef +import Data.Time (diffUTCTime, getCurrentTime) +import Patat.Presentation (PresentationCommand (..)) + + +-------------------------------------------------------------------------------- +-- | This function takes an existing channel for presentation commands +-- (presumably coming from human input) and creates a new one that /also/ sends +-- a 'Forward' command if nothing happens for N seconds. +autoAdvance + :: Int + -> Chan.Chan PresentationCommand + -> IO (Chan.Chan PresentationCommand) +autoAdvance delaySeconds existingChan = do + let delay = delaySeconds * 1000 -- We are working with ms in this function + + newChan <- Chan.newChan + latestCommandAt <- IORef.newIORef =<< getCurrentTime + + -- This is a thread that copies 'existingChan' to 'newChan', and writes + -- whenever the latest command was to 'latestCommandAt'. + _ <- forkIO $ forever $ do + cmd <- Chan.readChan existingChan + getCurrentTime >>= IORef.writeIORef latestCommandAt + Chan.writeChan newChan cmd + + -- This is a thread that waits around 'delay' seconds and then checks if + -- there's been a more recent command. If not, we write a 'Forward'. + _ <- forkIO $ forever $ do + current <- getCurrentTime + latest <- IORef.readIORef latestCommandAt + let elapsed = floor $ 1000 * (current `diffUTCTime` latest) :: Int + if elapsed >= delay + then do + Chan.writeChan newChan Forward + IORef.writeIORef latestCommandAt current + threadDelay (delay * 1000) + else do + let wait = delay - elapsed + threadDelay (wait * 1000) + + return newChan diff --git a/src/Patat/Images.hs b/src/Patat/Images.hs new file mode 100644 index 0000000..0d048d0 --- /dev/null +++ b/src/Patat/Images.hs @@ -0,0 +1,60 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE OverloadedStrings #-} +module Patat.Images + ( Backend + , Handle + , new + , drawImage + ) where + + +-------------------------------------------------------------------------------- +import Control.Exception (catch) +import qualified Data.Aeson as A +import qualified Data.Text as T +import Patat.Images.Internal +import qualified Patat.Images.ITerm2 as ITerm2 +import qualified Patat.Images.W3m as W3m +import Patat.Presentation.Internal + + +-------------------------------------------------------------------------------- +new :: ImageSettings -> IO Handle +new is + | isBackend is == "auto" = auto + | Just (Backend b) <- lookup (isBackend is) backends = + case A.fromJSON (A.Object $ isParams is) of + A.Success c -> b (Explicit c) + A.Error err -> fail $ + "Patat.Images.new: Error parsing config for " ++ + show (isBackend is) ++ " image backend: " ++ err +new is = fail $ + "Patat.Images.new: Could not find " ++ show (isBackend is) ++ + " image backend." + + +-------------------------------------------------------------------------------- +auto :: IO Handle +auto = go [] backends + where + go names ((name, Backend b) : bs) = catch + (b Auto) + (\(BackendNotSupported _) -> go (name : names) bs) + go names [] = fail $ + "Could not find a supported backend, tried: " ++ + T.unpack (T.intercalate ", " (reverse names)) + + +-------------------------------------------------------------------------------- +-- | All supported backends. We can use CPP to include or exclude some +-- depending on platform availability. +backends :: [(T.Text, Backend)] +backends = + [ ("iterm2", ITerm2.backend) + , ("w3m", W3m.backend) + ] + + +-------------------------------------------------------------------------------- +drawImage :: Handle -> FilePath -> IO () +drawImage = hDrawImage diff --git a/src/Patat/Images/ITerm2.hs b/src/Patat/Images/ITerm2.hs new file mode 100644 index 0000000..2584aed --- /dev/null +++ b/src/Patat/Images/ITerm2.hs @@ -0,0 +1,56 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE TemplateHaskell #-} +module Patat.Images.ITerm2 + ( backend + ) where + + +-------------------------------------------------------------------------------- +import Control.Exception (throwIO) +import Control.Monad (unless, when) +import qualified Data.Aeson as A +import qualified Data.ByteString.Base64.Lazy as B64 +import qualified Data.ByteString.Lazy as BL +import qualified Data.List as L +import qualified Patat.Images.Internal as Internal +import System.Environment (lookupEnv) + + +-------------------------------------------------------------------------------- +backend :: Internal.Backend +backend = Internal.Backend new + + +-------------------------------------------------------------------------------- +data Config = Config deriving (Eq) +instance A.FromJSON Config where parseJSON _ = return Config + + +-------------------------------------------------------------------------------- +new :: Internal.Config Config -> IO Internal.Handle +new config = do + when (config == Internal.Auto) $ do + termProgram <- lookupEnv "TERM_PROGRAM" + unless (termProgram == Just "iTerm.app") $ throwIO $ + Internal.BackendNotSupported "TERM_PROGRAM not iTerm.app" + + return Internal.Handle {Internal.hDrawImage = drawImage} + + +-------------------------------------------------------------------------------- +drawImage :: FilePath -> IO () +drawImage path = do + content <- BL.readFile path + withEscapeSequence $ do + putStr "1337;File=inline=1;width=100%;height=100%:" + BL.putStr (B64.encode content) + + +-------------------------------------------------------------------------------- +withEscapeSequence :: IO () -> IO () +withEscapeSequence f = do + term <- lookupEnv "TERM" + let inScreen = maybe False ("screen" `L.isPrefixOf`) term + putStr $ if inScreen then "\ESCPtmux;\ESC\ESC]" else "\ESC]" + f + putStrLn $ if inScreen then "\a\ESC\\" else "\a" diff --git a/src/Patat/Images/Internal.hs b/src/Patat/Images/Internal.hs new file mode 100644 index 0000000..939f962 --- /dev/null +++ b/src/Patat/Images/Internal.hs @@ -0,0 +1,39 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ExistentialQuantification #-} +module Patat.Images.Internal + ( Config (..) + , Backend (..) + , BackendNotSupported (..) + , Handle (..) + ) where + + +-------------------------------------------------------------------------------- +import Control.Exception (Exception) +import qualified Data.Aeson as A +import Data.Data (Data) +import Data.Typeable (Typeable) + + +-------------------------------------------------------------------------------- +data Config a = Auto | Explicit a deriving (Eq) + + +-------------------------------------------------------------------------------- +data Backend = forall a. A.FromJSON a => Backend (Config a -> IO Handle) + + +-------------------------------------------------------------------------------- +data BackendNotSupported = BackendNotSupported String + deriving (Data, Show, Typeable) + + +-------------------------------------------------------------------------------- +instance Exception BackendNotSupported + + +-------------------------------------------------------------------------------- +data Handle = Handle + { hDrawImage :: FilePath -> IO () + } diff --git a/src/Patat/Images/W3m.hs b/src/Patat/Images/W3m.hs new file mode 100644 index 0000000..d2ae171 --- /dev/null +++ b/src/Patat/Images/W3m.hs @@ -0,0 +1,145 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE TemplateHaskell #-} +module Patat.Images.W3m + ( backend + ) where + + +-------------------------------------------------------------------------------- +import Control.Exception (throwIO) +import Control.Monad (unless) +import qualified Data.Aeson.TH.Extended as A +import qualified Patat.Images.Internal as Internal +import qualified System.Directory as Directory +import qualified System.Process as Process +import Text.Read (readMaybe) + + +-------------------------------------------------------------------------------- +backend :: Internal.Backend +backend = Internal.Backend new + + +-------------------------------------------------------------------------------- +data Config = Config + { cPath :: Maybe FilePath + } deriving (Show) + + +-------------------------------------------------------------------------------- +new :: Internal.Config Config -> IO Internal.Handle +new config = do + w3m <- findW3m $ case config of + Internal.Explicit c -> cPath c + _ -> Nothing + + return Internal.Handle {Internal.hDrawImage = drawImage w3m} + + +-------------------------------------------------------------------------------- +newtype W3m = W3m FilePath deriving (Show) + + +-------------------------------------------------------------------------------- +findW3m :: Maybe FilePath -> IO W3m +findW3m mbPath + | Just path <- mbPath = do + exe <- isExecutable path + if exe + then return (W3m path) + else throwIO $ + Internal.BackendNotSupported $ path ++ " is not executable" + | otherwise = W3m <$> find paths + where + find [] = throwIO $ Internal.BackendNotSupported + "w3mimgdisplay executable not found" + find (p : ps) = do + exe <- isExecutable p + if exe then return p else find ps + + paths = + [ "/usr/lib/w3m/w3mimgdisplay" + , "/usr/libexec/w3m/w3mimgdisplay" + , "/usr/lib64/w3m/w3mimgdisplay" + , "/usr/libexec64/w3m/w3mimgdisplay" + , "/usr/local/libexec/w3m/w3mimgdisplay" + ] + + isExecutable path = do + exists <- Directory.doesFileExist path + if exists then do + perms <- Directory.getPermissions path + return (Directory.executable perms) + else + return False + + +-------------------------------------------------------------------------------- +-- | Parses something of the form " \n". +parseWidthHeight :: String -> Maybe (Int, Int) +parseWidthHeight output = case words output of + [ws, hs] | Just w <- readMaybe ws, Just h <- readMaybe hs -> + return (w, h) + _ -> Nothing + + +-------------------------------------------------------------------------------- +getTerminalSize :: W3m -> IO (Int, Int) +getTerminalSize (W3m w3mPath) = do + output <- Process.readProcess w3mPath ["-test"] "" + case parseWidthHeight output of + Just wh -> return wh + _ -> fail $ + "Patat.Images.W3m.getTerminalSize: " ++ + "Could not parse `w3mimgdisplay -test` output" + + +-------------------------------------------------------------------------------- +getImageSize :: W3m -> FilePath -> IO (Int, Int) +getImageSize (W3m w3mPath) path = do + output <- Process.readProcess w3mPath [] ("5;" ++ path ++ "\n") + case parseWidthHeight output of + Just wh -> return wh + _ -> fail $ + "Patat.Images.W3m.getImageSize: " ++ + "Could not parse image size using `w3mimgdisplay` for " ++ + path + + +-------------------------------------------------------------------------------- +drawImage :: W3m -> FilePath -> IO () +drawImage w3m@(W3m w3mPath) path = do + exists <- Directory.doesFileExist path + unless exists $ fail $ + "Patat.Images.W3m.drawImage: file does not exist: " ++ path + + tsize <- getTerminalSize w3m + isize <- getImageSize w3m path + let (x, y, w, h) = fit tsize isize + command = + "0;1;" ++ + show x ++ ";" ++ show y ++ ";" ++ show w ++ ";" ++ show h ++ + ";;;;;" ++ path ++ "\n4;\n3;\n" + + _ <- Process.readProcess w3mPath [] command + return () + where + fit :: (Int, Int) -> (Int, Int) -> (Int, Int, Int, Int) + fit (tw, th) (iw0, ih0) = + -- Scale down to width + let iw1 = if iw0 > tw then tw else iw0 + ih1 = if iw0 > tw then ((ih0 * tw) `div` iw0) else ih0 + + -- Scale down to height + iw2 = if ih1 > th then ((iw1 * th) `div` ih1) else iw1 + ih2 = if ih1 > th then th else ih1 + + -- Find position + x = (tw - iw2) `div` 2 + y = (th - ih2) `div` 2 in + + (x, y, iw2, ih2) + + +-------------------------------------------------------------------------------- +$(A.deriveFromJSON A.dropPrefixOptions ''Config) diff --git a/src/Patat/Presentation.hs b/src/Patat/Presentation.hs new file mode 100644 index 0000000..8da5a30 --- /dev/null +++ b/src/Patat/Presentation.hs @@ -0,0 +1,20 @@ +module Patat.Presentation + ( PresentationSettings (..) + , defaultPresentationSettings + + , Presentation (..) + , readPresentation + , displayPresentation + , displayPresentationError + , dumpPresentation + + , PresentationCommand (..) + , readPresentationCommand + , UpdatedPresentation (..) + , updatePresentation + ) where + +import Patat.Presentation.Display +import Patat.Presentation.Interactive +import Patat.Presentation.Internal +import Patat.Presentation.Read diff --git a/src/Patat/Presentation/Display.hs b/src/Patat/Presentation/Display.hs new file mode 100644 index 0000000..4e42c70 --- /dev/null +++ b/src/Patat/Presentation/Display.hs @@ -0,0 +1,377 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE CPP #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display + ( displayPresentation + , displayPresentationError + , dumpPresentation + ) where + + +-------------------------------------------------------------------------------- +import Control.Applicative ((<$>)) +import Control.Monad (mplus, unless) +import qualified Data.Aeson.Extended as A +import Data.Data.Extended (grecQ) +import qualified Data.List as L +import Data.Maybe (fromMaybe) +import Data.Monoid (mconcat, mempty, (<>)) +import qualified Data.Text as T +import qualified Patat.Images as Images +import Patat.Presentation.Display.CodeBlock +import Patat.Presentation.Display.Table +import Patat.Presentation.Internal +import Patat.PrettyPrint ((<$$>), (<+>)) +import qualified Patat.PrettyPrint as PP +import Patat.Theme (Theme (..)) +import qualified Patat.Theme as Theme +import Prelude +import qualified System.Console.ANSI as Ansi +import qualified System.Console.Terminal.Size as Terminal +import qualified System.IO as IO +import qualified Text.Pandoc.Extended as Pandoc + + +-------------------------------------------------------------------------------- +data CanvasSize = CanvasSize {csRows :: Int, csCols :: Int} deriving (Show) + + +-------------------------------------------------------------------------------- +-- | Display something within the presentation borders that draw the title and +-- the active slide number and so on. +displayWithBorders + :: Presentation -> (CanvasSize -> Theme -> PP.Doc) -> IO () +displayWithBorders Presentation {..} f = do + Ansi.clearScreen + Ansi.setCursorPosition 0 0 + + -- Get terminal width/title + mbWindow <- Terminal.size + let columns = fromMaybe 72 $ + (A.unFlexibleNum <$> psColumns pSettings) `mplus` + (Terminal.width <$> mbWindow) + rows = fromMaybe 24 $ + (A.unFlexibleNum <$> psRows pSettings) `mplus` + (Terminal.height <$> mbWindow) + + let settings = pSettings {psColumns = Just $ A.FlexibleNum columns} + theme = fromMaybe Theme.defaultTheme (psTheme settings) + title = PP.toString (prettyInlines theme pTitle) + titleWidth = length title + titleOffset = (columns - titleWidth) `div` 2 + borders = themed (themeBorders theme) + + unless (null title) $ do + let titleRemainder = columns - titleWidth - titleOffset + wrappedTitle = PP.spaces titleOffset <> PP.string title <> PP.spaces titleRemainder + PP.putDoc $ borders wrappedTitle + putStrLn "" + putStrLn "" + + let canvasSize = CanvasSize (rows - 2) columns + PP.putDoc $ formatWith settings $ f canvasSize theme + putStrLn "" + + let (sidx, _) = pActiveFragment + active = show (sidx + 1) ++ " / " ++ show (length pSlides) + activeWidth = length active + author = PP.toString (prettyInlines theme pAuthor) + authorWidth = length author + middleSpaces = PP.spaces $ columns - activeWidth - authorWidth - 2 + + Ansi.setCursorPosition (rows - 1) 0 + PP.putDoc $ borders $ PP.space <> PP.string author <> middleSpaces <> PP.string active <> PP.space + IO.hFlush IO.stdout + + +-------------------------------------------------------------------------------- +displayImage :: Images.Handle -> FilePath -> IO () +displayImage images path = do + Ansi.clearScreen + Ansi.setCursorPosition 0 0 + putStrLn "" + IO.hFlush IO.stdout + Images.drawImage images path + + +-------------------------------------------------------------------------------- +displayPresentation :: Maybe Images.Handle -> Presentation -> IO () +displayPresentation mbImages pres@Presentation {..} = + case getActiveFragment pres of + Nothing -> displayWithBorders pres mempty + Just (ActiveContent fragment) + | Just images <- mbImages + , Just image <- onlyImage fragment -> + displayImage images image + Just (ActiveContent fragment) -> + displayWithBorders pres $ \_canvasSize theme -> + prettyFragment theme fragment + Just (ActiveTitle block) -> + displayWithBorders pres $ \canvasSize theme -> + let pblock = prettyBlock theme block + (prows, pcols) = PP.dimensions pblock + (mLeft, mRight) = marginsOf pSettings + offsetRow = (csRows canvasSize `div` 2) - (prows `div` 2) + offsetCol = ((csCols canvasSize - mLeft - mRight) `div` 2) - (pcols `div` 2) + spaces = PP.NotTrimmable $ PP.spaces offsetCol in + mconcat (replicate (offsetRow - 3) PP.hardline) <$$> + PP.indent spaces spaces pblock + + where + -- Check if the fragment consists of just a single image, or a header and + -- some image. + onlyImage (Fragment blocks) + | [Pandoc.Para para] <- filter isVisibleBlock blocks + , [Pandoc.Image _ _ (target, _)] <- para = + Just target + onlyImage (Fragment blocks) + | [Pandoc.Header _ _ _, Pandoc.Para para] <- filter isVisibleBlock blocks + , [Pandoc.Image _ _ (target, _)] <- para = + Just target + onlyImage _ = Nothing + + +-------------------------------------------------------------------------------- +-- | Displays an error in the place of the presentation. This is useful if we +-- want to display an error but keep the presentation running. +displayPresentationError :: Presentation -> String -> IO () +displayPresentationError pres err = displayWithBorders pres $ \_ Theme {..} -> + themed themeStrong "Error occurred in the presentation:" <$$> + "" <$$> + (PP.string err) + + +-------------------------------------------------------------------------------- +dumpPresentation :: Presentation -> IO () +dumpPresentation pres = + let settings = pSettings pres + theme = fromMaybe Theme.defaultTheme (psTheme $ settings) in + PP.putDoc $ formatWith settings $ + PP.vcat $ L.intersperse "----------" $ do + slide <- pSlides pres + return $ case slide of + TitleSlide block -> "~~~title" <$$> prettyBlock theme block + ContentSlide fragments -> PP.vcat $ L.intersperse "~~~frag" $ do + fragment <- fragments + return $ prettyFragment theme fragment + + +-------------------------------------------------------------------------------- +formatWith :: PresentationSettings -> PP.Doc -> PP.Doc +formatWith ps = wrap . indent + where + (marginLeft, marginRight) = marginsOf ps + wrap = case (psWrap ps, psColumns ps) of + (Just True, Just (A.FlexibleNum col)) -> PP.wrapAt (Just $ col - marginRight) + _ -> id + spaces = PP.NotTrimmable $ PP.spaces marginLeft + indent = PP.indent spaces spaces + +-------------------------------------------------------------------------------- +prettyFragment :: Theme -> Fragment -> PP.Doc +prettyFragment theme fragment@(Fragment blocks) = + prettyBlocks theme blocks <> + case prettyReferences theme fragment of + [] -> mempty + refs -> PP.hardline <> PP.vcat refs + + +-------------------------------------------------------------------------------- +prettyBlock :: Theme -> Pandoc.Block -> PP.Doc + +prettyBlock theme (Pandoc.Plain inlines) = prettyInlines theme inlines + +prettyBlock theme (Pandoc.Para inlines) = + prettyInlines theme inlines <> PP.hardline + +prettyBlock theme@Theme {..} (Pandoc.Header i _ inlines) = + themed themeHeader (PP.string (replicate i '#') <+> prettyInlines theme inlines) <> + PP.hardline + +prettyBlock theme (Pandoc.CodeBlock (_, classes, _) txt) = + prettyCodeBlock theme classes txt + +prettyBlock theme (Pandoc.BulletList bss) = PP.vcat + [ PP.indent + (PP.NotTrimmable $ themed (themeBulletList theme) prefix) + (PP.Trimmable " ") + (prettyBlocks theme' bs) + | bs <- bss + ] <> PP.hardline + where + prefix = " " <> PP.string [marker] <> " " + marker = case T.unpack <$> themeBulletListMarkers theme of + Just (x : _) -> x + _ -> '-' + + -- Cycle the markers. + theme' = theme + { themeBulletListMarkers = + (\ls -> T.drop 1 ls <> T.take 1 ls) <$> themeBulletListMarkers theme + } + +prettyBlock theme@Theme {..} (Pandoc.OrderedList _ bss) = PP.vcat + [ PP.indent + (PP.NotTrimmable $ themed themeOrderedList $ PP.string prefix) + (PP.Trimmable " ") + (prettyBlocks theme bs) + | (prefix, bs) <- zip padded bss + ] <> PP.hardline + where + padded = [n ++ replicate (4 - length n) ' ' | n <- numbers] + numbers = + [ show i ++ "." + | i <- [1 .. length bss] + ] + +prettyBlock _theme (Pandoc.RawBlock _ t) = PP.string t <> PP.hardline + +prettyBlock _theme Pandoc.HorizontalRule = "---" + +prettyBlock theme@Theme {..} (Pandoc.BlockQuote bs) = + let quote = PP.NotTrimmable (themed themeBlockQuote "> ") in + PP.indent quote quote (prettyBlocks theme bs) + +prettyBlock theme@Theme {..} (Pandoc.DefinitionList terms) = + PP.vcat $ map prettyDefinition terms + where + prettyDefinition (term, definitions) = + themed themeDefinitionTerm (prettyInlines theme term) <$$> + PP.hardline <> PP.vcat + [ PP.indent + (PP.NotTrimmable (themed themeDefinitionList ": ")) + (PP.Trimmable " ") $ + prettyBlocks theme (Pandoc.plainToPara definition) + | definition <- definitions + ] + +prettyBlock theme (Pandoc.Table caption aligns _ headers rows) = + PP.wrapAt Nothing $ + prettyTable theme Table + { tCaption = prettyInlines theme caption + , tAligns = map align aligns + , tHeaders = map (prettyBlocks theme) headers + , tRows = map (map (prettyBlocks theme)) rows + } + where + align Pandoc.AlignLeft = PP.AlignLeft + align Pandoc.AlignCenter = PP.AlignCenter + align Pandoc.AlignDefault = PP.AlignLeft + align Pandoc.AlignRight = PP.AlignRight + +prettyBlock theme (Pandoc.Div _attrs blocks) = prettyBlocks theme blocks + +prettyBlock _theme Pandoc.Null = mempty + +#if MIN_VERSION_pandoc(1,18,0) +-- 'LineBlock' elements are new in pandoc-1.18 +prettyBlock theme@Theme {..} (Pandoc.LineBlock inliness) = + let ind = PP.NotTrimmable (themed themeLineBlock "| ") in + PP.wrapAt Nothing $ + PP.indent ind ind $ + PP.vcat $ + map (prettyInlines theme) inliness +#endif + + +-------------------------------------------------------------------------------- +prettyBlocks :: Theme -> [Pandoc.Block] -> PP.Doc +prettyBlocks theme = PP.vcat . map (prettyBlock theme) . filter isVisibleBlock + + +-------------------------------------------------------------------------------- +prettyInline :: Theme -> Pandoc.Inline -> PP.Doc + +prettyInline _theme Pandoc.Space = PP.space + +prettyInline _theme (Pandoc.Str str) = PP.string str + +prettyInline theme@Theme {..} (Pandoc.Emph inlines) = + themed themeEmph $ + prettyInlines theme inlines + +prettyInline theme@Theme {..} (Pandoc.Strong inlines) = + themed themeStrong $ + prettyInlines theme inlines + +prettyInline Theme {..} (Pandoc.Code _ txt) = + themed themeCode $ + PP.string (" " <> txt <> " ") + +prettyInline theme@Theme {..} link@(Pandoc.Link _attrs text (target, _title)) + | isReferenceLink link = + "[" <> themed themeLinkText (prettyInlines theme text) <> "]" + | otherwise = + "<" <> themed themeLinkTarget (PP.string target) <> ">" + +prettyInline _theme Pandoc.SoftBreak = PP.softline + +prettyInline _theme Pandoc.LineBreak = PP.hardline + +prettyInline theme@Theme {..} (Pandoc.Strikeout t) = + "~~" <> themed themeStrikeout (prettyInlines theme t) <> "~~" + +prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.SingleQuote t) = + "'" <> themed themeQuoted (prettyInlines theme t) <> "'" +prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.DoubleQuote t) = + "'" <> themed themeQuoted (prettyInlines theme t) <> "'" + +prettyInline Theme {..} (Pandoc.Math _ t) = + themed themeMath (PP.string t) + +prettyInline theme@Theme {..} (Pandoc.Image _attrs text (target, _title)) = + "![" <> themed themeImageText (prettyInlines theme text) <> "](" <> + themed themeImageTarget (PP.string target) <> ")" + +-- These elements aren't really supported. +prettyInline theme (Pandoc.Cite _ t) = prettyInlines theme t +prettyInline theme (Pandoc.Span _ t) = prettyInlines theme t +prettyInline _theme (Pandoc.RawInline _ t) = PP.string t +prettyInline theme (Pandoc.Note t) = prettyBlocks theme t +prettyInline theme (Pandoc.Superscript t) = prettyInlines theme t +prettyInline theme (Pandoc.Subscript t) = prettyInlines theme t +prettyInline theme (Pandoc.SmallCaps t) = prettyInlines theme t +-- prettyInline unsupported = PP.ondullred $ PP.string $ show unsupported + + +-------------------------------------------------------------------------------- +prettyInlines :: Theme -> [Pandoc.Inline] -> PP.Doc +prettyInlines theme = mconcat . map (prettyInline theme) + + +-------------------------------------------------------------------------------- +prettyReferences :: Theme -> Fragment -> [PP.Doc] +prettyReferences theme@Theme {..} = + map prettyReference . getReferences . unFragment + where + getReferences :: [Pandoc.Block] -> [Pandoc.Inline] + getReferences = filter isReferenceLink . grecQ + + prettyReference :: Pandoc.Inline -> PP.Doc + prettyReference (Pandoc.Link _attrs text (target, title)) = + "[" <> + themed themeLinkText (prettyInlines theme $ Pandoc.newlineToSpace text) <> + "](" <> + themed themeLinkTarget (PP.string target) <> + (if null title + then mempty + else PP.space <> "\"" <> PP.string title <> "\"") + <> ")" + prettyReference _ = mempty + + +-------------------------------------------------------------------------------- +isReferenceLink :: Pandoc.Inline -> Bool +isReferenceLink (Pandoc.Link _attrs text (target, _)) = + [Pandoc.Str target] /= text +isReferenceLink _ = False + + +-------------------------------------------------------------------------------- +isVisibleBlock :: Pandoc.Block -> Bool +isVisibleBlock Pandoc.Null = False +isVisibleBlock (Pandoc.RawBlock (Pandoc.Format "html") t) = + not ("" `L.isSuffixOf` t) +isVisibleBlock _ = True diff --git a/src/Patat/Presentation/Display/CodeBlock.hs b/src/Patat/Presentation/Display/CodeBlock.hs new file mode 100644 index 0000000..149bc68 --- /dev/null +++ b/src/Patat/Presentation/Display/CodeBlock.hs @@ -0,0 +1,83 @@ +-------------------------------------------------------------------------------- +-- | Displaying code blocks, optionally with syntax highlighting. +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display.CodeBlock + ( prettyCodeBlock + ) where + + +-------------------------------------------------------------------------------- +import Data.Maybe (mapMaybe) +import Data.Monoid (mconcat, (<>)) +import qualified Data.Text as T +import Patat.Presentation.Display.Table (themed) +import qualified Patat.PrettyPrint as PP +import Patat.Theme +import Prelude +import qualified Skylighting as Skylighting + + +-------------------------------------------------------------------------------- +highlight :: [String] -> String -> [Skylighting.SourceLine] +highlight classes rawCodeBlock = case mapMaybe getSyntax classes of + [] -> zeroHighlight rawCodeBlock + (syn : _) -> + case Skylighting.tokenize config syn (T.pack rawCodeBlock) of + Left _ -> zeroHighlight rawCodeBlock + Right sl -> sl + where + getSyntax :: String -> Maybe Skylighting.Syntax + getSyntax c = Skylighting.lookupSyntax (T.pack c) syntaxMap + + config :: Skylighting.TokenizerConfig + config = Skylighting.TokenizerConfig + { Skylighting.syntaxMap = syntaxMap + , Skylighting.traceOutput = False + } + + syntaxMap :: Skylighting.SyntaxMap + syntaxMap = Skylighting.defaultSyntaxMap + + +-------------------------------------------------------------------------------- +-- | This does fake highlighting, everything becomes a normal token. That makes +-- things a bit easier, since we only need to deal with one cases in the +-- renderer. +zeroHighlight :: String -> [Skylighting.SourceLine] +zeroHighlight str = + [[(Skylighting.NormalTok, T.pack line)] | line <- lines str] + + +-------------------------------------------------------------------------------- +prettyCodeBlock :: Theme -> [String] -> String -> PP.Doc +prettyCodeBlock theme@Theme {..} classes rawCodeBlock = + PP.vcat (map blockified sourceLines) <> + PP.hardline + where + sourceLines :: [Skylighting.SourceLine] + sourceLines = + [[]] ++ highlight classes rawCodeBlock ++ [[]] + + prettySourceLine :: Skylighting.SourceLine -> PP.Doc + prettySourceLine = mconcat . map prettyToken + + prettyToken :: Skylighting.Token -> PP.Doc + prettyToken (tokenType, str) = + themed (syntaxHighlight theme tokenType) (PP.string $ T.unpack str) + + sourceLineLength :: Skylighting.SourceLine -> Int + sourceLineLength line = sum [T.length str | (_, str) <- line] + + blockWidth :: Int + blockWidth = foldr max 0 (map sourceLineLength sourceLines) + + blockified :: Skylighting.SourceLine -> PP.Doc + blockified line = + let len = sourceLineLength line + indent = PP.NotTrimmable " " in + PP.indent indent indent $ + themed themeCodeBlock $ + " " <> + prettySourceLine line <> + PP.string (replicate (blockWidth - len) ' ') <> " " diff --git a/src/Patat/Presentation/Display/Table.hs b/src/Patat/Presentation/Display/Table.hs new file mode 100644 index 0000000..fee68c9 --- /dev/null +++ b/src/Patat/Presentation/Display/Table.hs @@ -0,0 +1,107 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Display.Table + ( Table (..) + , prettyTable + + , themed + ) where + + +-------------------------------------------------------------------------------- +import Data.List (intersperse, transpose) +import Data.Monoid (mconcat, mempty, (<>)) +import Patat.PrettyPrint ((<$$>)) +import qualified Patat.PrettyPrint as PP +import Patat.Theme (Theme (..)) +import qualified Patat.Theme as Theme +import Prelude + + +-------------------------------------------------------------------------------- +data Table = Table + { tCaption :: PP.Doc + , tAligns :: [PP.Alignment] + , tHeaders :: [PP.Doc] + , tRows :: [[PP.Doc]] + } + + +-------------------------------------------------------------------------------- +prettyTable + :: Theme -> Table -> PP.Doc +prettyTable theme@Theme {..} Table {..} = + PP.indent (PP.Trimmable " ") (PP.Trimmable " ") $ + lineIf (not isHeaderLess) (hcat2 headerHeight + [ themed themeTableHeader (PP.align w a (vpad headerHeight header)) + | (w, a, header) <- zip3 columnWidths tAligns tHeaders + ]) <> + dashedHeaderSeparator theme columnWidths <$$> + joinRows + [ hcat2 rowHeight + [ PP.align w a (vpad rowHeight cell) + | (w, a, cell) <- zip3 columnWidths tAligns row + ] + | (rowHeight, row) <- zip rowHeights tRows + ] <$$> + lineIf isHeaderLess (dashedHeaderSeparator theme columnWidths) <> + lineIf + (not $ PP.null tCaption) (PP.hardline <> "Table: " <> tCaption) + where + lineIf cond line = if cond then line <> PP.hardline else mempty + + joinRows + | all (all isSimpleCell) tRows = PP.vcat + | otherwise = PP.vcat . intersperse "" + + isHeaderLess = all PP.null tHeaders + + headerDimensions = map PP.dimensions tHeaders :: [(Int, Int)] + rowDimensions = map (map PP.dimensions) tRows :: [[(Int, Int)]] + + columnWidths :: [Int] + columnWidths = + [ safeMax (map snd col) + | col <- transpose (headerDimensions : rowDimensions) + ] + + rowHeights = map (safeMax . map fst) rowDimensions :: [Int] + headerHeight = safeMax (map fst headerDimensions) :: Int + + vpad :: Int -> PP.Doc -> PP.Doc + vpad height doc = + let (actual, _) = PP.dimensions doc in + doc <> mconcat (replicate (height - actual) PP.hardline) + + safeMax = foldr max 0 + + hcat2 :: Int -> [PP.Doc] -> PP.Doc + hcat2 rowHeight = PP.paste . intersperse (spaces2 rowHeight) + + spaces2 :: Int -> PP.Doc + spaces2 rowHeight = + mconcat $ intersperse PP.hardline $ + replicate rowHeight (PP.string " ") + + +-------------------------------------------------------------------------------- +isSimpleCell :: PP.Doc -> Bool +isSimpleCell = (<= 1) . fst . PP.dimensions + + +-------------------------------------------------------------------------------- +dashedHeaderSeparator :: Theme -> [Int] -> PP.Doc +dashedHeaderSeparator Theme {..} columnWidths = + mconcat $ intersperse (PP.string " ") + [ themed themeTableSeparator (PP.string (replicate w '-')) + | w <- columnWidths + ] + + +-------------------------------------------------------------------------------- +-- | This does not really belong in the module. +themed :: Maybe Theme.Style -> PP.Doc -> PP.Doc +themed Nothing = id +themed (Just (Theme.Style [])) = id +themed (Just (Theme.Style codes)) = PP.ansi codes diff --git a/src/Patat/Presentation/Fragment.hs b/src/Patat/Presentation/Fragment.hs new file mode 100644 index 0000000..0908381 --- /dev/null +++ b/src/Patat/Presentation/Fragment.hs @@ -0,0 +1,134 @@ +-- | For background info on the spec, see the "Incremental lists" section of the +-- the pandoc manual. +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveTraversable #-} +module Patat.Presentation.Fragment + ( FragmentSettings (..) + , fragmentBlocks + , fragmentBlock + ) where + +import Data.Foldable (Foldable) +import Data.List (foldl', intersperse) +import Data.Maybe (fromMaybe) +import Data.Traversable (Traversable) +import Prelude +import qualified Text.Pandoc as Pandoc + +data FragmentSettings = FragmentSettings + { fsIncrementalLists :: !Bool + } deriving (Show) + +-- fragmentBlocks :: [Pandoc.Block] -> [[Pandoc.Block]] +-- fragmentBlocks = NonEmpty.toList . joinFragmentedBlocks . map fragmentBlock +fragmentBlocks :: FragmentSettings -> [Pandoc.Block] -> [[Pandoc.Block]] +fragmentBlocks fs blocks0 = + case joinFragmentedBlocks (map (fragmentBlock fs) blocks0) of + Unfragmented bs -> [bs] + Fragmented xs bs -> map (fromMaybe []) xs ++ [fromMaybe [] bs] + +-- | This is all the ways we can "present" a block, after splitting in +-- fragments. +-- +-- In the simplest (and most common case) a block can only be presented in a +-- single way ('Unfragmented'). +-- +-- Alternatively, we might want to show different (partial) versions of the +-- block first before showing the final complete one. These partial or complete +-- versions can be empty, hence the 'Maybe'. +-- +-- For example, imagine that we display the following bullet list incrementally: +-- +-- > [1, 2, 3] +-- +-- Then we would get something like: +-- +-- > Fragmented [Nothing, Just [1], Just [1, 2]] (Just [1, 2, 3]) +data Fragmented a + = Unfragmented a + | Fragmented [Maybe a] (Maybe a) + deriving (Functor, Foldable, Show, Traversable) + +fragmentBlock :: FragmentSettings -> Pandoc.Block -> Fragmented Pandoc.Block +fragmentBlock _fs block@(Pandoc.Para inlines) + | inlines == threeDots = Fragmented [Nothing] Nothing + | otherwise = Unfragmented block + where + threeDots = intersperse Pandoc.Space $ replicate 3 (Pandoc.Str ".") + +fragmentBlock fs (Pandoc.BulletList bs0) = + fragmentList fs (fsIncrementalLists fs) Pandoc.BulletList bs0 + +fragmentBlock fs (Pandoc.OrderedList attr bs0) = + fragmentList fs (fsIncrementalLists fs) (Pandoc.OrderedList attr) bs0 + +fragmentBlock fs (Pandoc.BlockQuote [Pandoc.BulletList bs0]) = + fragmentList fs (not $ fsIncrementalLists fs) Pandoc.BulletList bs0 + +fragmentBlock fs (Pandoc.BlockQuote [Pandoc.OrderedList attr bs0]) = + fragmentList fs (not $ fsIncrementalLists fs) (Pandoc.OrderedList attr) bs0 + +fragmentBlock _ block@(Pandoc.BlockQuote _) = Unfragmented block + +fragmentBlock _ block@(Pandoc.Header _ _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.Plain _) = Unfragmented block +fragmentBlock _ block@(Pandoc.CodeBlock _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.RawBlock _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.DefinitionList _) = Unfragmented block +fragmentBlock _ block@(Pandoc.Table _ _ _ _ _) = Unfragmented block +fragmentBlock _ block@(Pandoc.Div _ _) = Unfragmented block +fragmentBlock _ block@Pandoc.HorizontalRule = Unfragmented block +fragmentBlock _ block@Pandoc.Null = Unfragmented block + +#if MIN_VERSION_pandoc(1,18,0) +fragmentBlock _ block@(Pandoc.LineBlock _) = Unfragmented block +#endif + +joinFragmentedBlocks :: [Fragmented block] -> Fragmented [block] +joinFragmentedBlocks = + foldl' append (Unfragmented []) + where + append (Unfragmented xs) (Unfragmented y) = + Unfragmented (xs ++ [y]) + + append (Fragmented xs x) (Unfragmented y) = + Fragmented xs (appendMaybe x (Just y)) + + append (Unfragmented x) (Fragmented ys y) = + Fragmented + [appendMaybe (Just x) y' | y' <- ys] + (appendMaybe (Just x) y) + + append (Fragmented xs x) (Fragmented ys y) = + Fragmented + (xs ++ [appendMaybe x y' | y' <- ys]) + (appendMaybe x y) + + appendMaybe :: Maybe [a] -> Maybe a -> Maybe [a] + appendMaybe Nothing Nothing = Nothing + appendMaybe Nothing (Just x) = Just [x] + appendMaybe (Just xs) Nothing = Just xs + appendMaybe (Just xs) (Just x) = Just (xs ++ [x]) + +fragmentList + :: FragmentSettings -- ^ Global settings + -> Bool -- ^ Fragment THIS list? + -> ([[Pandoc.Block]] -> Pandoc.Block) -- ^ List constructor + -> [[Pandoc.Block]] -- ^ List items + -> Fragmented Pandoc.Block -- ^ Resulting list +fragmentList fs fragmentThisList constructor blocks0 = + fmap constructor fragmented + where + -- The fragmented list per list item. + items :: [Fragmented [Pandoc.Block]] + items = map (joinFragmentedBlocks . map (fragmentBlock fs)) blocks0 + + fragmented :: Fragmented [[Pandoc.Block]] + fragmented = joinFragmentedBlocks $ + map (if fragmentThisList then insertPause else id) items + + insertPause :: Fragmented a -> Fragmented a + insertPause (Unfragmented x) = Fragmented [Nothing] (Just x) + insertPause (Fragmented xs x) = Fragmented (Nothing : xs) x diff --git a/src/Patat/Presentation/Interactive.hs b/src/Patat/Presentation/Interactive.hs new file mode 100644 index 0000000..d3977e3 --- /dev/null +++ b/src/Patat/Presentation/Interactive.hs @@ -0,0 +1,126 @@ +-------------------------------------------------------------------------------- +-- | Module that allows the user to interact with the presentation +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Interactive + ( PresentationCommand (..) + , readPresentationCommand + + , UpdatedPresentation (..) + , updatePresentation + ) where + + +-------------------------------------------------------------------------------- +import Patat.Presentation.Internal +import Patat.Presentation.Read + + +-------------------------------------------------------------------------------- +data PresentationCommand + = Exit + | Forward + | Backward + | SkipForward + | SkipBackward + | First + | Last + | Reload + | UnknownCommand String + + +-------------------------------------------------------------------------------- +readPresentationCommand :: IO PresentationCommand +readPresentationCommand = do + k <- readKey + case k of + "q" -> return Exit + "\n" -> return Forward + "\DEL" -> return Backward + "h" -> return Backward + "j" -> return SkipForward + "k" -> return SkipBackward + "l" -> return Forward + -- Arrow keys + "\ESC[C" -> return Forward + "\ESC[D" -> return Backward + "\ESC[B" -> return SkipForward + "\ESC[A" -> return SkipBackward + -- PageUp and PageDown + "\ESC[6" -> return Forward + "\ESC[5" -> return Backward + "0" -> return First + "G" -> return Last + "r" -> return Reload + _ -> return (UnknownCommand k) + where + readKey :: IO String + readKey = do + c0 <- getChar + case c0 of + '\ESC' -> do + c1 <- getChar + case c1 of + '[' -> do + c2 <- getChar + return [c0, c1, c2] + _ -> return [c0, c1] + _ -> return [c0] + + +-------------------------------------------------------------------------------- +data UpdatedPresentation + = UpdatedPresentation !Presentation + | ExitedPresentation + | ErroredPresentation String + deriving (Show) + + +-------------------------------------------------------------------------------- +updatePresentation + :: PresentationCommand -> Presentation -> IO UpdatedPresentation + +updatePresentation cmd presentation = case cmd of + Exit -> return ExitedPresentation + Forward -> return $ goToSlide $ \(s, f) -> (s, f + 1) + Backward -> return $ goToSlide $ \(s, f) -> (s, f - 1) + SkipForward -> return $ goToSlide $ \(s, _) -> (s + 10, 0) + SkipBackward -> return $ goToSlide $ \(s, _) -> (s - 10, 0) + First -> return $ goToSlide $ \_ -> (0, 0) + Last -> return $ goToSlide $ \_ -> (numSlides presentation, 0) + Reload -> reloadPresentation + UnknownCommand _ -> return (UpdatedPresentation presentation) + where + numSlides :: Presentation -> Int + numSlides pres = length (pSlides pres) + + clip :: Index -> Presentation -> Index + clip (slide, fragment) pres + | slide >= numSlides pres = (numSlides pres - 1, lastFragments - 1) + | slide < 0 = (0, 0) + | fragment >= numFragments' slide = + if slide + 1 >= numSlides pres + then (slide, lastFragments - 1) + else (slide + 1, 0) + | fragment < 0 = + if slide - 1 >= 0 + then (slide - 1, numFragments' (slide - 1) - 1) + else (slide, 0) + | otherwise = (slide, fragment) + where + numFragments' s = maybe 1 numFragments (getSlide s pres) + lastFragments = numFragments' (numSlides pres - 1) + + goToSlide :: (Index -> Index) -> UpdatedPresentation + goToSlide f = UpdatedPresentation $ presentation + { pActiveFragment = clip (f $ pActiveFragment presentation) presentation + } + + reloadPresentation = do + errOrPres <- readPresentation (pFilePath presentation) + return $ case errOrPres of + Left err -> ErroredPresentation err + Right pres -> UpdatedPresentation $ pres + { pActiveFragment = clip (pActiveFragment presentation) pres + } diff --git a/src/Patat/Presentation/Internal.hs b/src/Patat/Presentation/Internal.hs new file mode 100644 index 0000000..db8d16b --- /dev/null +++ b/src/Patat/Presentation/Internal.hs @@ -0,0 +1,266 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +module Patat.Presentation.Internal + ( Presentation (..) + , PresentationSettings (..) + , defaultPresentationSettings + + , Margins (..) + , marginsOf + + , ExtensionList (..) + , defaultExtensionList + + , ImageSettings (..) + + , Slide (..) + , Fragment (..) + , Index + + , getSlide + , numFragments + + , ActiveFragment (..) + , getActiveFragment + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (mplus) +import qualified Data.Aeson.Extended as A +import qualified Data.Aeson.TH.Extended as A +import qualified Data.Foldable as Foldable +import Data.List (intercalate) +import Data.Maybe (fromMaybe, listToMaybe) +import Data.Monoid (Monoid (..)) +import Data.Semigroup (Semigroup (..)) +import qualified Data.Text as T +import qualified Patat.Theme as Theme +import Prelude +import qualified Text.Pandoc as Pandoc +import Text.Read (readMaybe) + + +-------------------------------------------------------------------------------- +data Presentation = Presentation + { pFilePath :: !FilePath + , pTitle :: ![Pandoc.Inline] + , pAuthor :: ![Pandoc.Inline] + , pSettings :: !PresentationSettings + , pSlides :: [Slide] + , pActiveFragment :: !Index + } deriving (Show) + + +-------------------------------------------------------------------------------- +-- | These are patat-specific settings. That is where they differ from more +-- general metadata (author, title...) +data PresentationSettings = PresentationSettings + { psRows :: !(Maybe (A.FlexibleNum Int)) + , psColumns :: !(Maybe (A.FlexibleNum Int)) + , psMargins :: !(Maybe Margins) + , psWrap :: !(Maybe Bool) + , psTheme :: !(Maybe Theme.Theme) + , psIncrementalLists :: !(Maybe Bool) + , psAutoAdvanceDelay :: !(Maybe (A.FlexibleNum Int)) + , psSlideLevel :: !(Maybe Int) + , psPandocExtensions :: !(Maybe ExtensionList) + , psImages :: !(Maybe ImageSettings) + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance Semigroup PresentationSettings where + l <> r = PresentationSettings + { psRows = psRows l `mplus` psRows r + , psColumns = psColumns l `mplus` psColumns r + , psMargins = psMargins l <> psMargins r + , psWrap = psWrap l `mplus` psWrap r + , psTheme = psTheme l <> psTheme r + , psIncrementalLists = psIncrementalLists l `mplus` psIncrementalLists r + , psAutoAdvanceDelay = psAutoAdvanceDelay l `mplus` psAutoAdvanceDelay r + , psSlideLevel = psSlideLevel l `mplus` psSlideLevel r + , psPandocExtensions = psPandocExtensions l `mplus` psPandocExtensions r + , psImages = psImages l `mplus` psImages r + } + + +-------------------------------------------------------------------------------- +instance Monoid PresentationSettings where + mappend = (<>) + mempty = PresentationSettings + Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing + + +-------------------------------------------------------------------------------- +defaultPresentationSettings :: PresentationSettings +defaultPresentationSettings = PresentationSettings + { psRows = Nothing + , psColumns = Nothing + , psMargins = Just defaultMargins + , psWrap = Nothing + , psTheme = Just Theme.defaultTheme + , psIncrementalLists = Nothing + , psAutoAdvanceDelay = Nothing + , psSlideLevel = Nothing + , psPandocExtensions = Nothing + , psImages = Nothing + } + + +-------------------------------------------------------------------------------- +data Margins = Margins + { mLeft :: !(Maybe (A.FlexibleNum Int)) + , mRight :: !(Maybe (A.FlexibleNum Int)) + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance Semigroup Margins where + l <> r = Margins + { mLeft = mLeft l `mplus` mLeft r + , mRight = mRight l `mplus` mRight r + } + + +-------------------------------------------------------------------------------- +instance Monoid Margins where + mappend = (<>) + mempty = Margins Nothing Nothing + + +-------------------------------------------------------------------------------- +defaultMargins :: Margins +defaultMargins = Margins + { mLeft = Nothing + , mRight = Nothing + } + + +-------------------------------------------------------------------------------- +marginsOf :: PresentationSettings -> (Int, Int) +marginsOf presentationSettings = + (marginLeft, marginRight) + where + margins = fromMaybe defaultMargins $ psMargins presentationSettings + marginLeft = fromMaybe 0 (A.unFlexibleNum <$> mLeft margins) + marginRight = fromMaybe 0 (A.unFlexibleNum <$> mRight margins) + + +-------------------------------------------------------------------------------- +newtype ExtensionList = ExtensionList {unExtensionList :: Pandoc.Extensions} + deriving (Show) + + +-------------------------------------------------------------------------------- +instance A.FromJSON ExtensionList where + parseJSON = A.withArray "FromJSON ExtensionList" $ + fmap (ExtensionList . mconcat) . mapM parseExt . Foldable.toList + where + parseExt = A.withText "FromJSON ExtensionList" $ \txt -> case txt of + -- Our default extensions + "patat_extensions" -> return (unExtensionList defaultExtensionList) + + -- Individuals + _ -> case readMaybe ("Ext_" ++ T.unpack txt) of + Just e -> return $ Pandoc.extensionsFromList [e] + Nothing -> fail $ + "Unknown extension: " ++ show txt ++ + ", known extensions are: " ++ + intercalate ", " + [ show (drop 4 (show e)) + | e <- [minBound .. maxBound] :: [Pandoc.Extension] + ] + + +-------------------------------------------------------------------------------- +defaultExtensionList :: ExtensionList +defaultExtensionList = ExtensionList $ + Pandoc.readerExtensions Pandoc.def `mappend` Pandoc.extensionsFromList + [ Pandoc.Ext_yaml_metadata_block + , Pandoc.Ext_table_captions + , Pandoc.Ext_simple_tables + , Pandoc.Ext_multiline_tables + , Pandoc.Ext_grid_tables + , Pandoc.Ext_pipe_tables + , Pandoc.Ext_raw_html + , Pandoc.Ext_tex_math_dollars + , Pandoc.Ext_fenced_code_blocks + , Pandoc.Ext_fenced_code_attributes + , Pandoc.Ext_backtick_code_blocks + , Pandoc.Ext_inline_code_attributes + , Pandoc.Ext_fancy_lists + , Pandoc.Ext_four_space_rule + , Pandoc.Ext_definition_lists + , Pandoc.Ext_compact_definition_lists + , Pandoc.Ext_example_lists + , Pandoc.Ext_strikeout + , Pandoc.Ext_superscript + , Pandoc.Ext_subscript + ] + + +-------------------------------------------------------------------------------- +data ImageSettings = ImageSettings + { isBackend :: !T.Text + , isParams :: !A.Object + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance A.FromJSON ImageSettings where + parseJSON = A.withObject "FromJSON ImageSettings" $ \o -> do + t <- o A..: "backend" + return ImageSettings {isBackend = t, isParams = o} + + +-------------------------------------------------------------------------------- +data Slide + = ContentSlide [Fragment] + | TitleSlide Pandoc.Block + deriving (Show) + + +-------------------------------------------------------------------------------- +newtype Fragment = Fragment {unFragment :: [Pandoc.Block]} + deriving (Monoid, Semigroup, Show) + + +-------------------------------------------------------------------------------- +-- | Active slide, active fragment. +type Index = (Int, Int) + + +-------------------------------------------------------------------------------- +getSlide :: Int -> Presentation -> Maybe Slide +getSlide sidx = listToMaybe . drop sidx . pSlides + + +-------------------------------------------------------------------------------- +numFragments :: Slide -> Int +numFragments (ContentSlide fragments) = length fragments +numFragments (TitleSlide _) = 1 + + +-------------------------------------------------------------------------------- +data ActiveFragment = ActiveContent Fragment | ActiveTitle Pandoc.Block + deriving (Show) + + +-------------------------------------------------------------------------------- +getActiveFragment :: Presentation -> Maybe ActiveFragment +getActiveFragment presentation = do + let (sidx, fidx) = pActiveFragment presentation + slide <- getSlide sidx presentation + case slide of + TitleSlide block -> return (ActiveTitle block) + ContentSlide fragments -> + fmap ActiveContent . listToMaybe $ drop fidx fragments + + +-------------------------------------------------------------------------------- +$(A.deriveFromJSON A.dropPrefixOptions ''PresentationSettings) +$(A.deriveFromJSON A.dropPrefixOptions ''Margins) diff --git a/src/Patat/Presentation/Read.hs b/src/Patat/Presentation/Read.hs new file mode 100644 index 0000000..581c31d --- /dev/null +++ b/src/Patat/Presentation/Read.hs @@ -0,0 +1,205 @@ +-- | Read a presentation from disk. +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.Presentation.Read + ( readPresentation + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad.Except (ExceptT (..), runExceptT, + throwError) +import Control.Monad.Trans (liftIO) +import qualified Data.Aeson as A +import qualified Data.HashMap.Strict as HMS +import Data.Maybe (fromMaybe) +import Data.Monoid (mempty, (<>)) +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.Text.IO as T +import qualified Data.Yaml as Yaml +import Patat.Presentation.Fragment +import Patat.Presentation.Internal +import Prelude +import System.Directory (doesFileExist, getHomeDirectory) +import System.FilePath (takeExtension, ()) +import qualified Text.Pandoc.Error as Pandoc +import qualified Text.Pandoc.Extended as Pandoc + + +-------------------------------------------------------------------------------- +readPresentation :: FilePath -> IO (Either String Presentation) +readPresentation filePath = runExceptT $ do + -- We need to read the settings first. + src <- liftIO $ T.readFile filePath + homeSettings <- ExceptT readHomeSettings + metaSettings <- ExceptT $ return $ readMetaSettings src + let settings = metaSettings <> homeSettings <> defaultPresentationSettings + + let pexts = fromMaybe defaultExtensionList (psPandocExtensions settings) + reader <- case readExtension pexts ext of + Nothing -> throwError $ "Unknown file extension: " ++ show ext + Just x -> return x + doc <- case reader src of + Left e -> throwError $ "Could not parse document: " ++ show e + Right x -> return x + + ExceptT $ return $ pandocToPresentation filePath settings doc + where + ext = takeExtension filePath + + +-------------------------------------------------------------------------------- +readExtension + :: ExtensionList -> String + -> Maybe (T.Text -> Either Pandoc.PandocError Pandoc.Pandoc) +readExtension (ExtensionList extensions) fileExt = case fileExt of + ".md" -> Just $ Pandoc.runPure . Pandoc.readMarkdown readerOpts + ".lhs" -> Just $ Pandoc.runPure . Pandoc.readMarkdown lhsOpts + "" -> Just $ Pandoc.runPure . Pandoc.readMarkdown readerOpts + ".org" -> Just $ Pandoc.runPure . Pandoc.readOrg readerOpts + _ -> Nothing + + where + readerOpts = Pandoc.def + { Pandoc.readerExtensions = + extensions <> absolutelyRequiredExtensions + } + + lhsOpts = readerOpts + { Pandoc.readerExtensions = + Pandoc.readerExtensions readerOpts <> + Pandoc.extensionsFromList [Pandoc.Ext_literate_haskell] + } + + absolutelyRequiredExtensions = + Pandoc.extensionsFromList [Pandoc.Ext_yaml_metadata_block] + + +-------------------------------------------------------------------------------- +pandocToPresentation + :: FilePath -> PresentationSettings -> Pandoc.Pandoc + -> Either String Presentation +pandocToPresentation pFilePath pSettings pandoc@(Pandoc.Pandoc meta _) = do + let !pTitle = Pandoc.docTitle meta + !pSlides = pandocToSlides pSettings pandoc + !pActiveFragment = (0, 0) + !pAuthor = concat (Pandoc.docAuthors meta) + return Presentation {..} + + +-------------------------------------------------------------------------------- +-- | This re-parses the pandoc metadata block using the YAML library. This +-- avoids the problems caused by pandoc involving rendering Markdown. This +-- should only be used for settings though, not things like title / authors +-- since those /can/ contain markdown. +parseMetadataBlock :: T.Text -> Maybe A.Value +parseMetadataBlock src = do + block <- T.encodeUtf8 <$> mbBlock + either (const Nothing) Just (Yaml.decodeEither' block) + where + mbBlock :: Maybe T.Text + mbBlock = case T.lines src of + ("---" : ls) -> case break (`elem` ["---", "..."]) ls of + (_, []) -> Nothing + (block, (_ : _)) -> Just (T.unlines block) + _ -> Nothing + + +-------------------------------------------------------------------------------- +-- | Read settings from the metadata block in the Pandoc document. +readMetaSettings :: T.Text -> Either String PresentationSettings +readMetaSettings src = fromMaybe (Right mempty) $ do + A.Object obj <- parseMetadataBlock src + val <- HMS.lookup "patat" obj + return $! resultToEither $! A.fromJSON val + where + resultToEither :: A.Result a -> Either String a + resultToEither (A.Success x) = Right x + resultToEither (A.Error e) = Left $! + "Error parsing patat settings from metadata: " ++ e + + +-------------------------------------------------------------------------------- +-- | Read settings from "$HOME/.patat.yaml". +readHomeSettings :: IO (Either String PresentationSettings) +readHomeSettings = do + home <- getHomeDirectory + let path = home ".patat.yaml" + exists <- doesFileExist path + if not exists + then return (Right mempty) + else do + errOrPs <- Yaml.decodeFileEither path + return $! case errOrPs of + Left err -> Left (show err) + Right ps -> Right ps + + +-------------------------------------------------------------------------------- +pandocToSlides :: PresentationSettings -> Pandoc.Pandoc -> [Slide] +pandocToSlides settings pandoc = + let slideLevel = fromMaybe (detectSlideLevel pandoc) (psSlideLevel settings) + unfragmented = splitSlides slideLevel pandoc + fragmented = + [ case slide of + TitleSlide _ -> slide + ContentSlide fragments0 -> + let blocks = concatMap unFragment fragments0 + blockss = fragmentBlocks fragmentSettings blocks in + ContentSlide (map Fragment blockss) + | slide <- unfragmented + ] in + fragmented + where + fragmentSettings = FragmentSettings + { fsIncrementalLists = fromMaybe False (psIncrementalLists settings) + } + + +-------------------------------------------------------------------------------- +-- | Find level of header that starts slides. This is defined as the least +-- header that occurs before a non-header in the blocks. +detectSlideLevel :: Pandoc.Pandoc -> Int +detectSlideLevel (Pandoc.Pandoc _meta blocks0) = + go 6 blocks0 + where + go level (Pandoc.Header n _ _ : x : xs) + | n < level && nonHeader x = go n xs + | otherwise = go level (x:xs) + go level (_ : xs) = go level xs + go level [] = level + + nonHeader (Pandoc.Header _ _ _) = False + nonHeader _ = True + + +-------------------------------------------------------------------------------- +-- | Split a pandoc document into slides. If the document contains horizonal +-- rules, we use those as slide delimiters. If there are no horizontal rules, +-- we split using headers, determined by the slide level (see +-- 'detectSlideLevel'). +splitSlides :: Int -> Pandoc.Pandoc -> [Slide] +splitSlides slideLevel (Pandoc.Pandoc _meta blocks0) + | any (== Pandoc.HorizontalRule) blocks0 = splitAtRules blocks0 + | otherwise = splitAtHeaders [] blocks0 + where + mkContentSlide :: [Pandoc.Block] -> [Slide] + mkContentSlide [] = [] -- Never create empty slides + mkContentSlide bs = [ContentSlide [Fragment bs]] + + splitAtRules blocks = case break (== Pandoc.HorizontalRule) blocks of + (xs, []) -> mkContentSlide xs + (xs, (_rule : ys)) -> mkContentSlide xs ++ splitAtRules ys + + splitAtHeaders acc [] = + mkContentSlide (reverse acc) + splitAtHeaders acc (b@(Pandoc.Header i _ _) : bs) + | i > slideLevel = splitAtHeaders (b : acc) bs + | i == slideLevel = + mkContentSlide (reverse acc) ++ splitAtHeaders [b] bs + | otherwise = + mkContentSlide (reverse acc) ++ [TitleSlide b] ++ splitAtHeaders [] bs + splitAtHeaders acc (b : bs) = + splitAtHeaders (b : acc) bs diff --git a/src/Patat/PrettyPrint.hs b/src/Patat/PrettyPrint.hs new file mode 100644 index 0000000..bffa274 --- /dev/null +++ b/src/Patat/PrettyPrint.hs @@ -0,0 +1,411 @@ +-------------------------------------------------------------------------------- +-- | This is a small pretty-printing library. +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE RecordWildCards #-} +module Patat.PrettyPrint + ( Doc + , toString + , dimensions + , null + + , hPutDoc + , putDoc + + , string + , text + , space + , spaces + , softline + , hardline + + , wrapAt + + , Trimmable (..) + , indent + + , ansi + + , (<+>) + , (<$$>) + , vcat + + -- * Exotic combinators + , Alignment (..) + , align + , paste + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad.Reader (asks, local) +import Control.Monad.RWS (RWS, runRWS) +import Control.Monad.State (get, gets, modify) +import Control.Monad.Writer (tell) +import Data.Foldable (Foldable) +import qualified Data.List as L +import Data.Monoid (Monoid, mconcat, mempty) +import Data.Semigroup (Semigroup (..)) +import Data.String (IsString (..)) +import qualified Data.Text as T +import Data.Traversable (Traversable, traverse) +import Prelude hiding (null) +import qualified System.Console.ANSI as Ansi +import qualified System.IO as IO + + +-------------------------------------------------------------------------------- +-- | A simple chunk of text. All ANSI codes are "reset" after printing. +data Chunk + = StringChunk [Ansi.SGR] String + | NewlineChunk + deriving (Eq) + + +-------------------------------------------------------------------------------- +type Chunks = [Chunk] + + +-------------------------------------------------------------------------------- +hPutChunk :: IO.Handle -> Chunk -> IO () +hPutChunk h NewlineChunk = IO.hPutStrLn h "" +hPutChunk h (StringChunk codes str) = do + Ansi.hSetSGR h (reverse codes) + IO.hPutStr h str + Ansi.hSetSGR h [Ansi.Reset] + + +-------------------------------------------------------------------------------- +chunkToString :: Chunk -> String +chunkToString NewlineChunk = "\n" +chunkToString (StringChunk _ str) = str + + +-------------------------------------------------------------------------------- +-- | If two neighboring chunks have the same set of ANSI codes, we can group +-- them together. +optimizeChunks :: Chunks -> Chunks +optimizeChunks (StringChunk c1 s1 : StringChunk c2 s2 : chunks) + | c1 == c2 = optimizeChunks (StringChunk c1 (s1 <> s2) : chunks) + | otherwise = + StringChunk c1 s1 : optimizeChunks (StringChunk c2 s2 : chunks) +optimizeChunks (x : chunks) = x : optimizeChunks chunks +optimizeChunks [] = [] + + +-------------------------------------------------------------------------------- +chunkLines :: Chunks -> [Chunks] +chunkLines chunks = case break (== NewlineChunk) chunks of + (xs, _newline : ys) -> xs : chunkLines ys + (xs, []) -> [xs] + + +-------------------------------------------------------------------------------- +data DocE + = String String + | Softspace + | Hardspace + | Softline + | Hardline + | WrapAt + { wrapAtCol :: Maybe Int + , wrapDoc :: Doc + } + | Ansi + { ansiCode :: [Ansi.SGR] -> [Ansi.SGR] -- ^ Modifies current codes. + , ansiDoc :: Doc + } + | Indent + { indentFirstLine :: LineBuffer + , indentOtherLines :: LineBuffer + , indentDoc :: Doc + } + + +-------------------------------------------------------------------------------- +chunkToDocE :: Chunk -> DocE +chunkToDocE NewlineChunk = Hardline +chunkToDocE (StringChunk codes str) = Ansi (\_ -> codes) (Doc [String str]) + + +-------------------------------------------------------------------------------- +newtype Doc = Doc {unDoc :: [DocE]} + deriving (Monoid, Semigroup) + + +-------------------------------------------------------------------------------- +instance IsString Doc where + fromString = string + + +-------------------------------------------------------------------------------- +instance Show Doc where + show = toString + + +-------------------------------------------------------------------------------- +data DocEnv = DocEnv + { deCodes :: [Ansi.SGR] -- ^ Most recent ones first in the list + , deIndent :: LineBuffer -- ^ Don't need to store first-line indent + , deWrap :: Maybe Int -- ^ Wrap at columns + } + + +-------------------------------------------------------------------------------- +type DocM = RWS DocEnv Chunks LineBuffer + + +-------------------------------------------------------------------------------- +data Trimmable a + = NotTrimmable !a + | Trimmable !a + deriving (Foldable, Functor, Traversable) + + +-------------------------------------------------------------------------------- +-- | Note that this is reversed so we have fast append +type LineBuffer = [Trimmable Chunk] + + +-------------------------------------------------------------------------------- +bufferToChunks :: LineBuffer -> Chunks +bufferToChunks = map trimmableToChunk . reverse . dropWhile isTrimmable + where + isTrimmable (NotTrimmable _) = False + isTrimmable (Trimmable _) = True + + trimmableToChunk (NotTrimmable c) = c + trimmableToChunk (Trimmable c) = c + + +-------------------------------------------------------------------------------- +docToChunks :: Doc -> Chunks +docToChunks doc0 = + let env0 = DocEnv [] [] Nothing + ((), b, cs) = runRWS (go $ unDoc doc0) env0 mempty in + optimizeChunks (cs <> bufferToChunks b) + where + go :: [DocE] -> DocM () + + go [] = return () + + go (String str : docs) = do + chunk <- makeChunk str + modify (NotTrimmable chunk :) + go docs + + go (Softspace : docs) = do + hard <- softConversion Softspace docs + go (hard : docs) + + go (Hardspace : docs) = do + chunk <- makeChunk " " + modify (NotTrimmable chunk :) + go docs + + go (Softline : docs) = do + hard <- softConversion Softline docs + go (hard : docs) + + go (Hardline : docs) = do + buffer <- get + tell $ bufferToChunks buffer <> [NewlineChunk] + indentation <- asks deIndent + modify $ \_ -> if L.null docs then [] else indentation + go docs + + go (WrapAt {..} : docs) = do + local (\env -> env {deWrap = wrapAtCol}) $ go (unDoc wrapDoc) + go docs + + go (Ansi {..} : docs) = do + local (\env -> env {deCodes = ansiCode (deCodes env)}) $ + go (unDoc ansiDoc) + go docs + + go (Indent {..} : docs) = do + local (\env -> env {deIndent = indentOtherLines ++ deIndent env}) $ do + modify (indentFirstLine ++) + go (unDoc indentDoc) + go docs + + makeChunk :: String -> DocM Chunk + makeChunk str = do + codes <- asks deCodes + return $ StringChunk codes str + + -- Convert 'Softspace' or 'Softline' to 'Hardspace' or 'Hardline' + softConversion :: DocE -> [DocE] -> DocM DocE + softConversion soft docs = do + mbWrapCol <- asks deWrap + case mbWrapCol of + Nothing -> return hard + Just maxCol -> do + -- Slow. + currentLine <- gets (concatMap chunkToString . bufferToChunks) + let currentCol = length currentLine + case nextWordLength docs of + Nothing -> return hard + Just l + | currentCol + 1 + l <= maxCol -> return Hardspace + | otherwise -> return Hardline + where + hard = case soft of + Softspace -> Hardspace + Softline -> Hardline + _ -> soft + + nextWordLength :: [DocE] -> Maybe Int + nextWordLength [] = Nothing + nextWordLength (String x : xs) + | L.null x = nextWordLength xs + | otherwise = Just (length x) + nextWordLength (Softspace : xs) = nextWordLength xs + nextWordLength (Hardspace : xs) = nextWordLength xs + nextWordLength (Softline : xs) = nextWordLength xs + nextWordLength (Hardline : _) = Nothing + nextWordLength (WrapAt {..} : xs) = nextWordLength (unDoc wrapDoc ++ xs) + nextWordLength (Ansi {..} : xs) = nextWordLength (unDoc ansiDoc ++ xs) + nextWordLength (Indent {..} : xs) = nextWordLength (unDoc indentDoc ++ xs) + + +-------------------------------------------------------------------------------- +toString :: Doc -> String +toString = concat . map chunkToString . docToChunks + + +-------------------------------------------------------------------------------- +-- | Returns the rows and columns necessary to render this document +dimensions :: Doc -> (Int, Int) +dimensions doc = + let ls = lines (toString doc) in + (length ls, foldr max 0 (map length ls)) + + +-------------------------------------------------------------------------------- +null :: Doc -> Bool +null doc = case unDoc doc of [] -> True; _ -> False + + +-------------------------------------------------------------------------------- +hPutDoc :: IO.Handle -> Doc -> IO () +hPutDoc h = mapM_ (hPutChunk h) . docToChunks + + +-------------------------------------------------------------------------------- +putDoc :: Doc -> IO () +putDoc = hPutDoc IO.stdout + + +-------------------------------------------------------------------------------- +mkDoc :: DocE -> Doc +mkDoc e = Doc [e] + + +-------------------------------------------------------------------------------- +string :: String -> Doc +string = mkDoc . String -- TODO (jaspervdj): Newline conversion + + +-------------------------------------------------------------------------------- +text :: T.Text -> Doc +text = string . T.unpack + + +-------------------------------------------------------------------------------- +space :: Doc +space = mkDoc Softspace + + +-------------------------------------------------------------------------------- +spaces :: Int -> Doc +spaces n = mconcat $ replicate n space + + +-------------------------------------------------------------------------------- +softline :: Doc +softline = mkDoc Softline + + +-------------------------------------------------------------------------------- +hardline :: Doc +hardline = mkDoc Hardline + + +-------------------------------------------------------------------------------- +wrapAt :: Maybe Int -> Doc -> Doc +wrapAt wrapAtCol wrapDoc = mkDoc WrapAt {..} + + +-------------------------------------------------------------------------------- +indent :: Trimmable Doc -> Trimmable Doc -> Doc -> Doc +indent firstLineDoc otherLinesDoc doc = mkDoc $ Indent + { indentFirstLine = traverse docToChunks firstLineDoc + , indentOtherLines = traverse docToChunks otherLinesDoc + , indentDoc = doc + } + + +-------------------------------------------------------------------------------- +ansi :: [Ansi.SGR] -> Doc -> Doc +ansi codes = mkDoc . Ansi (codes ++) + + +-------------------------------------------------------------------------------- +(<+>) :: Doc -> Doc -> Doc +x <+> y = x <> space <> y +infixr 6 <+> + + +-------------------------------------------------------------------------------- +(<$$>) :: Doc -> Doc -> Doc +x <$$> y = x <> hardline <> y +infixr 5 <$$> + + +-------------------------------------------------------------------------------- +vcat :: [Doc] -> Doc +vcat = mconcat . L.intersperse hardline + + +-------------------------------------------------------------------------------- +data Alignment = AlignLeft | AlignCenter | AlignRight deriving (Eq, Ord, Show) + + +-------------------------------------------------------------------------------- +align :: Int -> Alignment -> Doc -> Doc +align width alignment doc0 = + let chunks0 = docToChunks doc0 + lines_ = chunkLines chunks0 in + vcat + [ Doc (map chunkToDocE (alignLine line)) + | line <- lines_ + ] + where + lineWidth :: [Chunk] -> Int + lineWidth = sum . map (length . chunkToString) + + alignLine :: [Chunk] -> [Chunk] + alignLine line = + let actual = lineWidth line + chunkSpaces n = [StringChunk [] (replicate n ' ')] in + case alignment of + AlignLeft -> line <> chunkSpaces (width - actual) + AlignRight -> chunkSpaces (width - actual) <> line + AlignCenter -> + let r = (width - actual) `div` 2 + l = (width - actual) - r in + chunkSpaces l <> line <> chunkSpaces r + + +-------------------------------------------------------------------------------- +-- | Like the unix program 'paste'. +paste :: [Doc] -> Doc +paste docs0 = + let chunkss = map docToChunks docs0 :: [Chunks] + cols = map chunkLines chunkss :: [[Chunks]] + rows0 = L.transpose cols :: [[Chunks]] + rows1 = map (map (Doc . map chunkToDocE)) rows0 :: [[Doc]] in + vcat $ map mconcat rows1 diff --git a/src/Patat/Theme.hs b/src/Patat/Theme.hs new file mode 100644 index 0000000..952a521 --- /dev/null +++ b/src/Patat/Theme.hs @@ -0,0 +1,324 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +module Patat.Theme + ( Theme (..) + , defaultTheme + + , Style (..) + + , SyntaxHighlighting (..) + , defaultSyntaxHighlighting + , syntaxHighlight + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (forM_, mplus) +import qualified Data.Aeson as A +import qualified Data.Aeson.TH.Extended as A +import Data.Char (toLower, toUpper) +import Data.Colour.SRGB (RGB(..), sRGB24reads, toSRGB24) +import Data.List (intercalate, isPrefixOf, isSuffixOf) +import qualified Data.Map as M +import Data.Maybe (mapMaybe, maybeToList) +import Data.Monoid (Monoid (..)) +import Data.Semigroup (Semigroup (..)) +import qualified Data.Text as T +import Numeric (showHex) +import Prelude +import qualified Skylighting as Skylighting +import qualified System.Console.ANSI as Ansi +import Text.Read (readMaybe) + + +-------------------------------------------------------------------------------- +data Theme = Theme + { themeBorders :: !(Maybe Style) + , themeHeader :: !(Maybe Style) + , themeCodeBlock :: !(Maybe Style) + , themeBulletList :: !(Maybe Style) + , themeBulletListMarkers :: !(Maybe T.Text) + , themeOrderedList :: !(Maybe Style) + , themeBlockQuote :: !(Maybe Style) + , themeDefinitionTerm :: !(Maybe Style) + , themeDefinitionList :: !(Maybe Style) + , themeTableHeader :: !(Maybe Style) + , themeTableSeparator :: !(Maybe Style) + , themeLineBlock :: !(Maybe Style) + , themeEmph :: !(Maybe Style) + , themeStrong :: !(Maybe Style) + , themeCode :: !(Maybe Style) + , themeLinkText :: !(Maybe Style) + , themeLinkTarget :: !(Maybe Style) + , themeStrikeout :: !(Maybe Style) + , themeQuoted :: !(Maybe Style) + , themeMath :: !(Maybe Style) + , themeImageText :: !(Maybe Style) + , themeImageTarget :: !(Maybe Style) + , themeSyntaxHighlighting :: !(Maybe SyntaxHighlighting) + } deriving (Show) + + +-------------------------------------------------------------------------------- +instance Semigroup Theme where + l <> r = Theme + { themeBorders = mplusOn themeBorders + , themeHeader = mplusOn themeHeader + , themeCodeBlock = mplusOn themeCodeBlock + , themeBulletList = mplusOn themeBulletList + , themeBulletListMarkers = mplusOn themeBulletListMarkers + , themeOrderedList = mplusOn themeOrderedList + , themeBlockQuote = mplusOn themeBlockQuote + , themeDefinitionTerm = mplusOn themeDefinitionTerm + , themeDefinitionList = mplusOn themeDefinitionList + , themeTableHeader = mplusOn themeTableHeader + , themeTableSeparator = mplusOn themeTableSeparator + , themeLineBlock = mplusOn themeLineBlock + , themeEmph = mplusOn themeEmph + , themeStrong = mplusOn themeStrong + , themeCode = mplusOn themeCode + , themeLinkText = mplusOn themeLinkText + , themeLinkTarget = mplusOn themeLinkTarget + , themeStrikeout = mplusOn themeStrikeout + , themeQuoted = mplusOn themeQuoted + , themeMath = mplusOn themeMath + , themeImageText = mplusOn themeImageText + , themeImageTarget = mplusOn themeImageTarget + , themeSyntaxHighlighting = mappendOn themeSyntaxHighlighting + } + where + mplusOn f = f l `mplus` f r + mappendOn f = f l `mappend` f r + + +-------------------------------------------------------------------------------- +instance Monoid Theme where + mappend = (<>) + mempty = Theme + Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing Nothing + +-------------------------------------------------------------------------------- +defaultTheme :: Theme +defaultTheme = Theme + { themeBorders = dull Ansi.Yellow + , themeHeader = dull Ansi.Blue + , themeCodeBlock = dull Ansi.White `mappend` ondull Ansi.Black + , themeBulletList = dull Ansi.Magenta + , themeBulletListMarkers = Just "-*" + , themeOrderedList = dull Ansi.Magenta + , themeBlockQuote = dull Ansi.Green + , themeDefinitionTerm = dull Ansi.Blue + , themeDefinitionList = dull Ansi.Magenta + , themeTableHeader = dull Ansi.Blue + , themeTableSeparator = dull Ansi.Magenta + , themeLineBlock = dull Ansi.Magenta + , themeEmph = dull Ansi.Green + , themeStrong = dull Ansi.Red `mappend` bold + , themeCode = dull Ansi.White `mappend` ondull Ansi.Black + , themeLinkText = dull Ansi.Green + , themeLinkTarget = dull Ansi.Cyan `mappend` underline + , themeStrikeout = ondull Ansi.Red + , themeQuoted = dull Ansi.Green + , themeMath = dull Ansi.Green + , themeImageText = dull Ansi.Green + , themeImageTarget = dull Ansi.Cyan `mappend` underline + , themeSyntaxHighlighting = Just defaultSyntaxHighlighting + } + where + dull c = Just $ Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c] + ondull c = Just $ Style [Ansi.SetColor Ansi.Background Ansi.Dull c] + bold = Just $ Style [Ansi.SetConsoleIntensity Ansi.BoldIntensity] + underline = Just $ Style [Ansi.SetUnderlining Ansi.SingleUnderline] + + +-------------------------------------------------------------------------------- +newtype Style = Style {unStyle :: [Ansi.SGR]} + deriving (Monoid, Semigroup, Show) + + +-------------------------------------------------------------------------------- +instance A.ToJSON Style where + toJSON = A.toJSON . mapMaybe sgrToString . unStyle + + +-------------------------------------------------------------------------------- +instance A.FromJSON Style where + parseJSON val = do + names <- A.parseJSON val + sgrs <- mapM toSgr names + return $! Style sgrs + where + toSgr name = case stringToSgr name of + Just sgr -> return sgr + Nothing -> fail $! + "Unknown style: " ++ show name ++ ". Known styles are: " ++ + intercalate ", " (map show $ M.keys namedSgrs) ++ + ", or \"rgb#RrGgBb\" and \"onRgb#RrGgBb\" where 'Rr', " ++ + "'Gg' and 'Bb' are hexadecimal bytes (e.g. \"rgb#f08000\")." + + +-------------------------------------------------------------------------------- +stringToSgr :: String -> Maybe Ansi.SGR +stringToSgr s + | "rgb#" `isPrefixOf` s = rgbToSgr Ansi.Foreground $ drop 4 s + | "onRgb#" `isPrefixOf` s = rgbToSgr Ansi.Background $ drop 6 s + | otherwise = M.lookup s namedSgrs + + +-------------------------------------------------------------------------------- +rgbToSgr :: Ansi.ConsoleLayer -> String -> Maybe Ansi.SGR +rgbToSgr layer rgbHex = + case sRGB24reads rgbHex of + [(color, "")] -> Just $ Ansi.SetRGBColor layer color + _ -> Nothing + + +-------------------------------------------------------------------------------- +sgrToString :: Ansi.SGR -> Maybe String +sgrToString (Ansi.SetColor layer intensity color) = Just $ + (\str -> case layer of + Ansi.Foreground -> str + Ansi.Background -> "on" ++ capitalize str) $ + (case intensity of + Ansi.Dull -> "dull" + Ansi.Vivid -> "vivid") ++ + (case color of + Ansi.Black -> "Black" + Ansi.Red -> "Red" + Ansi.Green -> "Green" + Ansi.Yellow -> "Yellow" + Ansi.Blue -> "Blue" + Ansi.Magenta -> "Magenta" + Ansi.Cyan -> "Cyan" + Ansi.White -> "White") + +sgrToString (Ansi.SetUnderlining Ansi.SingleUnderline) = Just "underline" + +sgrToString (Ansi.SetConsoleIntensity Ansi.BoldIntensity) = Just "bold" + +sgrToString (Ansi.SetItalicized True) = Just "italic" + +sgrToString (Ansi.SetRGBColor layer color) = Just $ + (\str -> case layer of + Ansi.Foreground -> str + Ansi.Background -> "on" ++ capitalize str) $ + "rgb#" ++ (toRGBHex $ toSRGB24 color) + where + toRGBHex (RGB r g b) = concat $ map toHexByte [r, g, b] + toHexByte x = showHex2 x "" + showHex2 x | x <= 0xf = ("0" ++) . showHex x + | otherwise = showHex x + +sgrToString _ = Nothing + + +-------------------------------------------------------------------------------- +namedSgrs :: M.Map String Ansi.SGR +namedSgrs = M.fromList + [ (name, sgr) + | sgr <- knownSgrs + , name <- maybeToList (sgrToString sgr) + ] + where + -- | It doesn't really matter if we generate "too much" SGRs here since + -- 'sgrToString' will only pick the ones we support. + knownSgrs = + [ Ansi.SetColor l i c + | l <- [minBound .. maxBound] + , i <- [minBound .. maxBound] + , c <- [minBound .. maxBound] + ] ++ + [Ansi.SetUnderlining u | u <- [minBound .. maxBound]] ++ + [Ansi.SetConsoleIntensity c | c <- [minBound .. maxBound]] ++ + [Ansi.SetItalicized i | i <- [minBound .. maxBound]] + + +-------------------------------------------------------------------------------- +newtype SyntaxHighlighting = SyntaxHighlighting + { unSyntaxHighlighting :: M.Map String Style + } deriving (Monoid, Semigroup, Show, A.ToJSON) + + +-------------------------------------------------------------------------------- +instance A.FromJSON SyntaxHighlighting where + parseJSON val = do + styleMap <- A.parseJSON val + forM_ (M.keys styleMap) $ \k -> case nameToTokenType k of + Just _ -> return () + Nothing -> fail $ "Unknown token type: " ++ show k + return (SyntaxHighlighting styleMap) + + +-------------------------------------------------------------------------------- +defaultSyntaxHighlighting :: SyntaxHighlighting +defaultSyntaxHighlighting = mkSyntaxHighlighting + [ (Skylighting.KeywordTok, dull Ansi.Yellow) + , (Skylighting.ControlFlowTok, dull Ansi.Yellow) + + , (Skylighting.DataTypeTok, dull Ansi.Green) + + , (Skylighting.DecValTok, dull Ansi.Red) + , (Skylighting.BaseNTok, dull Ansi.Red) + , (Skylighting.FloatTok, dull Ansi.Red) + , (Skylighting.ConstantTok, dull Ansi.Red) + , (Skylighting.CharTok, dull Ansi.Red) + , (Skylighting.SpecialCharTok, dull Ansi.Red) + , (Skylighting.StringTok, dull Ansi.Red) + , (Skylighting.VerbatimStringTok, dull Ansi.Red) + , (Skylighting.SpecialStringTok, dull Ansi.Red) + + , (Skylighting.CommentTok, dull Ansi.Blue) + , (Skylighting.DocumentationTok, dull Ansi.Blue) + , (Skylighting.AnnotationTok, dull Ansi.Blue) + , (Skylighting.CommentVarTok, dull Ansi.Blue) + + , (Skylighting.ImportTok, dull Ansi.Cyan) + , (Skylighting.OperatorTok, dull Ansi.Cyan) + , (Skylighting.FunctionTok, dull Ansi.Cyan) + , (Skylighting.PreprocessorTok, dull Ansi.Cyan) + ] + where + dull c = Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c] + + mkSyntaxHighlighting ls = SyntaxHighlighting $ + M.fromList [(nameForTokenType tt, s) | (tt, s) <- ls] + + +-------------------------------------------------------------------------------- +nameForTokenType :: Skylighting.TokenType -> String +nameForTokenType = + unCapitalize . dropTok . show + where + unCapitalize (x : xs) = toLower x : xs + unCapitalize xs = xs + + dropTok :: String -> String + dropTok str + | "Tok" `isSuffixOf` str = take (length str - 3) str + | otherwise = str + + +-------------------------------------------------------------------------------- +nameToTokenType :: String -> Maybe Skylighting.TokenType +nameToTokenType = readMaybe . capitalize . (++ "Tok") + + +-------------------------------------------------------------------------------- +capitalize :: String -> String +capitalize "" = "" +capitalize (x : xs) = toUpper x : xs + + +-------------------------------------------------------------------------------- +syntaxHighlight :: Theme -> Skylighting.TokenType -> Maybe Style +syntaxHighlight theme tokenType = do + sh <- themeSyntaxHighlighting theme + M.lookup (nameForTokenType tokenType) (unSyntaxHighlighting sh) + + +-------------------------------------------------------------------------------- +$(A.deriveJSON A.dropPrefixOptions ''Theme) diff --git a/src/Text/Pandoc/Extended.hs b/src/Text/Pandoc/Extended.hs new file mode 100644 index 0000000..941d716 --- /dev/null +++ b/src/Text/Pandoc/Extended.hs @@ -0,0 +1,30 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE LambdaCase #-} +module Text.Pandoc.Extended + ( module Text.Pandoc + + , plainToPara + , newlineToSpace + ) where + + +-------------------------------------------------------------------------------- +import Data.Data.Extended (grecT) +import Text.Pandoc +import Prelude + + +-------------------------------------------------------------------------------- +plainToPara :: [Block] -> [Block] +plainToPara = map $ \case + Plain inlines -> Para inlines + block -> block + + +-------------------------------------------------------------------------------- +newlineToSpace :: [Inline] -> [Inline] +newlineToSpace = grecT $ \case + SoftBreak -> Space + LineBreak -> Space + inline -> inline diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..8cbd382 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,15 @@ +resolver: 'lts-13.0' +save-hackage-creds: false + +packages: +- '.' + +flags: + patat: + patat-make-man: true + +extra-deps: +- 'pandoc-2.6' +- 'ipynb-0.1' +- 'ansi-terminal-0.9' +- 'ansi-wl-pprint-0.6.8.2@rev:1' diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..bbe7c5a --- /dev/null +++ b/test.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -o nounset -o errexit -o pipefail + +srcs=$(find tests -type f ! -name '*.dump') +stuff_went_wrong=false + +for src in $srcs; do + expected="$src.dump" + echo -n "Testing $src... " + actual=$(mktemp) + HOME=/dev/null patat --dump --force "$src" >"$actual" + + if [[ $@ == "--fix" ]]; then + cp "$actual" "$expected" + echo 'Fixed' + elif [[ ! -f "$expected" ]]; then + echo "missing file: $expected" + stuff_went_wrong=true + elif [[ "$(cat "$expected")" == "$(cat "$actual")" ]]; then + echo 'OK' + else + echo 'files differ' + diff "$actual" "$expected" || true + stuff_went_wrong=true + fi +done + +if [[ "$stuff_went_wrong" = true ]]; then + exit 1 +fi diff --git a/tests/01.md b/tests/01.md new file mode 100644 index 0000000..2fbdde2 --- /dev/null +++ b/tests/01.md @@ -0,0 +1,14 @@ +--- +title: This is my presentation +author: Jasper Van der Jeugt +... + +# This is a test + +Hello world + +--- + +# This is a second slide + +lololol diff --git a/tests/01.md.dump b/tests/01.md.dump new file mode 100644 index 0000000..1ae41da --- /dev/null +++ b/tests/01.md.dump @@ -0,0 +1,8 @@ +# This is a test + +Hello world + +---------- +# This is a second slide + +lololol diff --git a/tests/02.lhs b/tests/02.lhs new file mode 100644 index 0000000..fd7a5d3 --- /dev/null +++ b/tests/02.lhs @@ -0,0 +1,6 @@ +This is how you define a `String` in Haskell: + +> test :: String +> test = "Hello World!" + +Cool, right? diff --git a/tests/02.lhs.dump b/tests/02.lhs.dump new file mode 100644 index 0000000..d9e7171 --- /dev/null +++ b/tests/02.lhs.dump @@ -0,0 +1,8 @@ +This is how you define a  String  in Haskell: + +   +  test :: String  +  test = "Hello World!"  +   + +Cool, right? diff --git a/tests/03.md b/tests/03.md new file mode 100644 index 0000000..6b3ae16 --- /dev/null +++ b/tests/03.md @@ -0,0 +1,46 @@ +Inline markups: + +- ~~striked out~~ +- + +--- + +> Some quote + +> Quote with embedded list: +> +> - Hello +> - World + +--- + +- List with an embedded quote: + + > Tu quoque + + Wow rad stuff. + +- Second item in that list. + +--- + +Code with empty line: + + puts "wow" + + puts "amaze" + +--- + +Code in ordered list: + +1. Do you know the coolest codes? + + It's this: + + fire_missiles() + cancel() + + Great + +2. Also `fib` is pretty cool yeah diff --git a/tests/03.md.dump b/tests/03.md.dump new file mode 100644 index 0000000..e8b6b69 --- /dev/null +++ b/tests/03.md.dump @@ -0,0 +1,48 @@ +Inline markups: + + - ~~striked out~~ + - <http://example.com> + +---------- +> Some quote + +> Quote with embedded list: +>  +>  - Hello +>  - World + +---------- + - List with an embedded quote: + + > Tu quoque + + Wow rad stuff. + + - Second item in that list. + + +---------- +Code with empty line: + +   +  puts "wow"  +   +  puts "amaze"  +   + +---------- +Code in ordered list: + +1. Do you know the coolest codes? + + It's this: + +   +  fire_missiles()  +  cancel()  +   + + Great + +2. Also  fib  is pretty cool yeah + diff --git a/tests/bolditalic.md b/tests/bolditalic.md new file mode 100644 index 0000000..f680dc1 --- /dev/null +++ b/tests/bolditalic.md @@ -0,0 +1,8 @@ +--- +patat: + theme: + emph: [italic] + strong: [bold] +... + +**Strong** and _emph_. diff --git a/tests/bolditalic.md.dump b/tests/bolditalic.md.dump new file mode 100644 index 0000000..0a17414 --- /dev/null +++ b/tests/bolditalic.md.dump @@ -0,0 +1 @@ +Strong and emph. diff --git a/tests/comments.md b/tests/comments.md new file mode 100644 index 0000000..36ab949 --- /dev/null +++ b/tests/comments.md @@ -0,0 +1,16 @@ +# This is a test + +Hello world + + + +# This is a second slide + + + +Where are my raw blocks at + + diff --git a/tests/comments.md.dump b/tests/comments.md.dump new file mode 100644 index 0000000..296a5ac --- /dev/null +++ b/tests/comments.md.dump @@ -0,0 +1,8 @@ +# This is a test + +Hello world + +---------- +# This is a second slide + +Where are my raw blocks at diff --git a/tests/deflist.md b/tests/deflist.md new file mode 100644 index 0000000..81aee19 --- /dev/null +++ b/tests/deflist.md @@ -0,0 +1,20 @@ +Term 1 + +: Definition 1 + +Term 2 with *inline markup* + +: Definition 2 + + { some code, part of Definition 2 } + + Third paragraph of definition 2. + +--- + +Term 1 + ~ Definition 1 + +Term 2 + ~ Definition 2a + ~ Definition 2b diff --git a/tests/deflist.md.dump b/tests/deflist.md.dump new file mode 100644 index 0000000..8089fda --- /dev/null +++ b/tests/deflist.md.dump @@ -0,0 +1,24 @@ +Term 1 + +: Definition 1 + +Term 2 with inline markup + +: Definition 2 + +   +  { some code, part of Definition 2 }  +   + + Third paragraph of definition 2. + +---------- +Term 1 + +: Definition 1 + +Term 2 + +: Definition 2a + +: Definition 2b diff --git a/tests/extentions0.md b/tests/extentions0.md new file mode 100644 index 0000000..a001311 --- /dev/null +++ b/tests/extentions0.md @@ -0,0 +1,9 @@ +--- +patat: + pandocExtensions: + - patat_extensions + - autolink_bare_uris + - emoji +... + +Check out this example: http://example.com/ :smile: diff --git a/tests/extentions0.md.dump b/tests/extentions0.md.dump new file mode 100644 index 0000000..9e8b1a6 --- /dev/null +++ b/tests/extentions0.md.dump @@ -0,0 +1 @@ +Check out this example: <http://example.com/> 😄 diff --git a/tests/extentions1.md b/tests/extentions1.md new file mode 100644 index 0000000..62c770b --- /dev/null +++ b/tests/extentions1.md @@ -0,0 +1,7 @@ +--- +patat: + pandocExtensions: + - emoji +... + +The patat default ~~strikeout~~ is not enabled, but emojis are :smile: diff --git a/tests/extentions1.md.dump b/tests/extentions1.md.dump new file mode 100644 index 0000000..26b7986 --- /dev/null +++ b/tests/extentions1.md.dump @@ -0,0 +1 @@ +The patat default ~~strikeout~~ is not enabled, but emojis are 😄 diff --git a/tests/fragments.md b/tests/fragments.md new file mode 100644 index 0000000..510baa2 --- /dev/null +++ b/tests/fragments.md @@ -0,0 +1,27 @@ +--- +patat: + incrementalLists: true +... + +- This list +- is displayed + + * item + * by item + +- Or sometimes + + > * all at + > * once + +--- + +Legen + +. . . + +wait for it + +. . . + +Dary! diff --git a/tests/fragments.md.dump b/tests/fragments.md.dump new file mode 100644 index 0000000..c29b455 --- /dev/null +++ b/tests/fragments.md.dump @@ -0,0 +1,54 @@ + + +~~~frag + - This list + +~~~frag + - This list + - is displayed + + + + +~~~frag + - This list + - is displayed + +  * item + + +~~~frag + - This list + - is displayed + +  * item +  * by item + + +~~~frag + - This list + - is displayed + +  * item +  * by item + + - Or sometimes + +  * all at +  * once + + +---------- +Legen + +~~~frag +Legen + +wait for it + +~~~frag +Legen + +wait for it + +Dary! diff --git a/tests/headers.md b/tests/headers.md new file mode 100644 index 0000000..73d9ea5 --- /dev/null +++ b/tests/headers.md @@ -0,0 +1,15 @@ +# This could be a title + +## This is nested + +Here is some content + +## This is also nested + +Here is more content + +# Another topic + +## What is going on? + +I think we can display slides? diff --git a/tests/headers.md.dump b/tests/headers.md.dump new file mode 100644 index 0000000..2b52c98 --- /dev/null +++ b/tests/headers.md.dump @@ -0,0 +1,21 @@ +~~~title +# This could be a title + +---------- +## This is nested + +Here is some content + +---------- +## This is also nested + +Here is more content + +---------- +~~~title +# Another topic + +---------- +## What is going on? + +I think we can display slides? diff --git a/tests/links.md b/tests/links.md new file mode 100644 index 0000000..153f959 --- /dev/null +++ b/tests/links.md @@ -0,0 +1,8 @@ +This is an "automatic link": . + +This is an [inline link](/url), and here's [one with +a title](http://fsf.org "click here for a good time!"). + +Let's talk about [foo][foosite] + +[foosite]: http://foo.com/ diff --git a/tests/links.md.dump b/tests/links.md.dump new file mode 100644 index 0000000..2862e9a --- /dev/null +++ b/tests/links.md.dump @@ -0,0 +1,10 @@ +This is an "automatic link": <https://jaspervdj.be>. + +This is an [inline link], and here's [one with +a title]. + +Let's talk about [foo] + +[inline link](/url) +[one with a title](http://fsf.org "click here for a good time!") +[foo](http://foo.com/) \ No newline at end of file diff --git a/tests/lists.md b/tests/lists.md new file mode 100644 index 0000000..d534704 --- /dev/null +++ b/tests/lists.md @@ -0,0 +1,13 @@ +- This is a nested list. + + * The nested items should have different list markers. + + * I mean, they can be the same, but it doesn't look nice. + + printf("Nested code block!\n") + + * Cool right? + + Definitely super cool + +- One final item diff --git a/tests/lists.md.dump b/tests/lists.md.dump new file mode 100644 index 0000000..1305289 --- /dev/null +++ b/tests/lists.md.dump @@ -0,0 +1,15 @@ + - This is a nested list. + +  * The nested items should have different list markers. + +  * I mean, they can be the same, but it doesn't look nice. + + printf("Nested code block!\n") + +  * Cool right? + + Definitely super cool + + + - One final item + diff --git a/tests/margins.md b/tests/margins.md new file mode 100644 index 0000000..5d0a59c --- /dev/null +++ b/tests/margins.md @@ -0,0 +1,17 @@ +--- +patat: + wrap: true + columns: 57 # 10 + 42 + 5 + margins: + left: 10 + right: 5 +... + +This text will have 10 spaces on the left. + +- So + * will + * these + * bullets + +This line will have 10 spaces on the left, but will also break after "left". diff --git a/tests/margins.md.dump b/tests/margins.md.dump new file mode 100644 index 0000000..5c3117b --- /dev/null +++ b/tests/margins.md.dump @@ -0,0 +1,10 @@ + This text will have 10 spaces on the left. +  +  - So +  * will +  * these +  * bullets + +  + This line will have 10 spaces on the left, + but will also break after "left". diff --git a/tests/meta.md b/tests/meta.md new file mode 100644 index 0000000..2ba5db9 --- /dev/null +++ b/tests/meta.md @@ -0,0 +1,12 @@ +--- +patat: + theme: + bulletListMarkers: '<>' +... + +- Hello +- World + * How + * Are + * You + * Doing diff --git a/tests/meta.md.dump b/tests/meta.md.dump new file mode 100644 index 0000000..740ed6b --- /dev/null +++ b/tests/meta.md.dump @@ -0,0 +1,7 @@ + < Hello + < World +  > How +  > Are +  > You +  > Doing + diff --git a/tests/slidelevel0.md b/tests/slidelevel0.md new file mode 100644 index 0000000..b07adab --- /dev/null +++ b/tests/slidelevel0.md @@ -0,0 +1,12 @@ +--- +patat: + slideLevel: 0 +--- + +# We should not split slides + +Never + +# At all + +Because we have `slideLevel` set to 0 diff --git a/tests/slidelevel0.md.dump b/tests/slidelevel0.md.dump new file mode 100644 index 0000000..c31c2e0 --- /dev/null +++ b/tests/slidelevel0.md.dump @@ -0,0 +1,7 @@ +# We should not split slides + +Never + +# At all + +Because we have  slideLevel  set to 0 diff --git a/tests/slidelevel1.md b/tests/slidelevel1.md new file mode 100644 index 0000000..dc531c4 --- /dev/null +++ b/tests/slidelevel1.md @@ -0,0 +1,26 @@ +--- +patat: + slideLevel: 1 +--- + +# This starts a new slide + +## But this does not + +Here is some content + +## And another header + +And more content (yep) + +# This should start a new slide + +## With some content + +### Very deeply nested + +#### Is a hidden message + +##### A dark secret... + +jet fuel can't melt steel beams diff --git a/tests/slidelevel1.md.dump b/tests/slidelevel1.md.dump new file mode 100644 index 0000000..3aa8af5 --- /dev/null +++ b/tests/slidelevel1.md.dump @@ -0,0 +1,22 @@ +# This starts a new slide + +## But this does not + +Here is some content + +## And another header + +And more content (yep) + +---------- +# This should start a new slide + +## With some content + +### Very deeply nested + +#### Is a hidden message + +##### A dark secret... + +jet fuel can't melt steel beams diff --git a/tests/slidelevel2.md b/tests/slidelevel2.md new file mode 100644 index 0000000..25e8795 --- /dev/null +++ b/tests/slidelevel2.md @@ -0,0 +1,15 @@ +# This is a title + +## This is a slide + +Here is some content + +## And another slide + +And more content (yep) + +# This is another title + +## With some content + +Yay diff --git a/tests/slidelevel2.md.dump b/tests/slidelevel2.md.dump new file mode 100644 index 0000000..1a400f2 --- /dev/null +++ b/tests/slidelevel2.md.dump @@ -0,0 +1,21 @@ +~~~title +# This is a title + +---------- +## This is a slide + +Here is some content + +---------- +## And another slide + +And more content (yep) + +---------- +~~~title +# This is another title + +---------- +## With some content + +Yay diff --git a/tests/syntax.md b/tests/syntax.md new file mode 100644 index 0000000..f6c803d --- /dev/null +++ b/tests/syntax.md @@ -0,0 +1,14 @@ +--- +patat: + theme: + syntaxHighlighting: + decVal: [bold, onDullRed] +... + +Some simple code: + +```c +int main(int argc, char **argv) { + return 0; +} +``` diff --git a/tests/syntax.md.dump b/tests/syntax.md.dump new file mode 100644 index 0000000..eb4893f --- /dev/null +++ b/tests/syntax.md.dump @@ -0,0 +1,7 @@ +Some simple code: + +   +  int main(int argc, char **argv) {  +  return 0;  +  }  +   diff --git a/tests/tables.md b/tests/tables.md new file mode 100644 index 0000000..fe7d72e --- /dev/null +++ b/tests/tables.md @@ -0,0 +1,48 @@ +# Normal simple table + + Right Left Center Default +------- ------ ---------- ------- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1 + +Table: Demonstration of simple table syntax. + + +# Headerless table + +------- ------ ---------- ------- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1 +------- ------ ---------- ------- + +# Multiline + +------------------------------------------------------------- + Centered Default Right Left + Header Aligned Aligned Aligned +----------- ------- --------------- ------------------------- + First row 12.0 Example of a row that + spans multiple lines. + + Second row 5.0 Here's another one. Note + the blank line between + rows. +------------------------------------------------------------- + +Table: Here's the caption. It, too, may span +multiple lines. + +# Headerless multiline + +----------- ------- --------------- ------------------------- + First row 12.0 Example of a row that + spans multiple lines. + + Second row 5.0 Here's another one. Note + the blank line between + rows. +----------- ------- --------------- ------------------------- + +: Here's a multiline table without headers. diff --git a/tests/tables.md.dump b/tests/tables.md.dump new file mode 100644 index 0000000..0b0a93f --- /dev/null +++ b/tests/tables.md.dump @@ -0,0 +1,48 @@ +# Normal simple table + + Right Left Center Default + ----- ---- ------ ------- + 12 12 12 12  + 123 123 123 123  + 1 1 1 1  + + Table: Demonstration of simple table syntax. + +---------- +# Headerless table + + --- --- --- --- + 12 12 12 12 + 123 123 123 123 + 1 1 1 1  + --- --- --- --- + +---------- +# Multiline + + Centered Default Right Left  + Header Aligned Aligned Aligned  + -------- ------- ------- ------------------------ + First row 12.0 Example of a row that  + spans multiple lines.  +  + Second row 5.0 Here's another one. Note + the blank line between  + rows.  + + Table: Here's the caption. It, too, may span + multiple lines. + +---------- +# Headerless multiline + + ------ --- ---- ------------------------ + First row 12.0 Example of a row that  + spans multiple lines.  +  + Second row 5.0 Here's another one. Note + the blank line between  + rows.  + ------ --- ---- ------------------------ + + Table: Here's a multiline table without headers. diff --git a/tests/themes.md b/tests/themes.md new file mode 100644 index 0000000..ca2958c --- /dev/null +++ b/tests/themes.md @@ -0,0 +1,12 @@ +--- +patat: + theme: + bulletListMarkers: '-+' + emph: [onVividRed, underline] + strong: [rgb#f08000, onRgb#101060] +... + +- This is a simple list. + * With _nested_ items. + * One or two **bold**. +- The list theming is customized a bit. diff --git a/tests/themes.md.dump b/tests/themes.md.dump new file mode 100644 index 0000000..f68c671 --- /dev/null +++ b/tests/themes.md.dump @@ -0,0 +1,5 @@ + - This is a simple list. +  + With nested items. +  + One or two bold. + + - The list theming is customized a bit. diff --git a/tests/wrapping.md b/tests/wrapping.md new file mode 100644 index 0000000..bcffc16 --- /dev/null +++ b/tests/wrapping.md @@ -0,0 +1,25 @@ +--- +patat: + wrap: true + columns: 40 +... + +This is a long +sentence over multiple +lines which can be +re-wrapped. + + +This is a super long sentence over a single line which should also be re-wrapped. + + + This is a table and tables should not be wrapped + ------- ------- ---------- ---------- ---------- + 1 2 3 4 5 + 6 7 8 9 10 + +- This is a list +- This list has a really long sentence in it which should also be wrapped with proper indentation +- Another item + +This line is long, and then ends with `code` diff --git a/tests/wrapping.md.dump b/tests/wrapping.md.dump new file mode 100644 index 0000000..d44e767 --- /dev/null +++ b/tests/wrapping.md.dump @@ -0,0 +1,20 @@ +This is a long sentence over multiple +lines which can be re-wrapped. + +This is a super long sentence over a +single line which should also be +re-wrapped. + + This is a table and tables should not be wrapped + ------- ------- ---------- ---------- ---------- + 1 2 3 4 5  + 6 7 8 9 10  + + - This is a list + - This list has a really long sentence + in it which should also be wrapped + with proper indentation + - Another item + +This line is long, and then ends with + code  -- 2.30.2